NOISSUE reformat and sanitize ganalytics

This commit is contained in:
Petr Mrázek 2016-11-20 12:32:27 +01:00
parent 2ec15c32e4
commit 2f8c752d1f
11 changed files with 808 additions and 1034 deletions

View File

@ -4,7 +4,24 @@ find_package(Qt5Core)
find_package(Qt5Gui)
find_package(Qt5Network)
add_library(ganalytics STATIC ganalytics.cpp ganalytics.h)
set(ganalytics_SOURCES
src/ganalytics.cpp
src/ganalytics_worker.cpp
src/ganalytics_worker.h
src/sys.h
include/ganalytics.h
)
if (WIN32)
list(APPEND ganalytics_SOURCES src/sys_win32.cpp)
elseif (UNIX)
if(APPLE)
list(APPEND ganalytics_SOURCES src/sys_apple.cpp)
else()
list(APPEND ganalytics_SOURCES src/sys_unix.cpp)
endif()
endif()
add_library(ganalytics STATIC ${ganalytics_SOURCES})
qt5_use_modules(ganalytics Core Gui Network)
target_include_directories(ganalytics PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(ganalytics PRIVATE -DQT_GUI_LIB)
target_include_directories(ganalytics PUBLIC include)

View File

@ -1,923 +0,0 @@
#include "ganalytics.h"
#include <QCoreApplication>
#include <QDataStream>
#include <QDateTime>
#include <QDebug>
#include <QLocale>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QQueue>
#include <QSettings>
#include <QTimer>
#include <QUrlQuery>
#include <QUuid>
#ifdef QT_GUI_LIB
#include <QScreen>
#include <QGuiApplication>
#endif // QT_GUI_LIB
#ifdef QT_QML_LIB
#include <QQmlEngine>
#include <QQmlContext>
#endif // QT_QML_LIB
struct QueryBuffer
{
QUrlQuery postQuery;
QDateTime time;
};
/**
* Class Private
* Private members and functions.
*/
class GAnalytics::Private : public QObject
{
Q_OBJECT
public:
explicit Private(GAnalytics *parent = 0);
~Private();
GAnalytics *q;
QNetworkAccessManager *networkManager;
QQueue<QueryBuffer> messageQueue;
QTimer timer;
QNetworkRequest request;
GAnalytics::LogLevel logLevel;
QString trackingID;
QString clientID;
QString userID;
QString appName;
QString appVersion;
QString language;
QString screenResolution;
QString viewportSize;
bool isSending;
const static int fourHours = 4 * 60 * 60 * 1000;
const static QString dateTimeFormat;
public:
void logMessage(GAnalytics::LogLevel level, const QString &message);
QUrlQuery buildStandardPostQuery(const QString &type);
#ifdef QT_GUI_LIB
QString getScreenResolution();
#endif // QT_GUI_LIB
QString getUserAgent();
QString getSystemInfo();
QList<QString> persistMessageQueue();
void readMessagesFromFile(const QList<QString> &dataList);
QString getClientID();
QString getUserID();
void setUserID(const QString &userID);
void enqueQueryWithCurrentTime(const QUrlQuery &query);
void setIsSending(bool doSend);
signals:
void postNextMessage();
public slots:
void postMessage();
void postMessageFinished();
};
const QString GAnalytics::Private::dateTimeFormat = "yyyy,MM,dd-hh:mm::ss:zzz";
/**
* Constructor
* Constructs an object of class Private.
* @param parent
*/
GAnalytics::Private::Private(GAnalytics *parent)
: QObject(parent)
, q(parent)
, networkManager(NULL)
, request(QUrl("http://www.google-analytics.com/collect"))
, logLevel(GAnalytics::Error)
, isSending(false)
{
clientID = getClientID();
userID = getUserID();
language = QLocale::system().name().toLower().replace("_", "-");
#ifdef QT_GUI_LIB
screenResolution = getScreenResolution();
#endif // QT_GUI_LIB
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
appName = QCoreApplication::instance()->applicationName();
appVersion = QCoreApplication::instance()->applicationVersion();
request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
connect(this, SIGNAL(postNextMessage()), this, SLOT(postMessage()));
timer.start(30000);
connect(&timer, SIGNAL(timeout()), this, SLOT(postMessage()));
}
/**
* Destructor
* Delete an object of class Private.
*/
GAnalytics::Private::~Private()
{
}
void GAnalytics::Private::logMessage(LogLevel level, const QString &message)
{
if (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 GAnalytics::Private::buildStandardPostQuery(const QString &type)
{
QUrlQuery query;
query.addQueryItem("v", "1");
query.addQueryItem("tid", trackingID);
query.addQueryItem("cid", clientID);
if(!userID.isEmpty())
{
query.addQueryItem("uid", userID);
}
query.addQueryItem("t", type);
query.addQueryItem("ul", language);
#ifdef QT_GUI_LIB
query.addQueryItem("vp", viewportSize);
query.addQueryItem("sr", screenResolution);
#endif // QT_GUI_LIB
return query;
}
#ifdef QT_GUI_LIB
/**
* Get devicese screen resolution.
* @return A QString like "800x600".
*/
QString GAnalytics::Private::getScreenResolution()
{
QScreen *screen = QGuiApplication::primaryScreen();
QSize size = screen->size();
return QString("%1x%2").arg(size.width()).arg(size.height());
}
#endif // QT_GUI_LIB
/**
* 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 GAnalytics::Private::getUserAgent()
{
QString locale = QLocale::system().name();
QString system = getSystemInfo();
return QString("%1/%2 (%3; %4) GAnalytics/1.0 (Qt/%5)").arg(appName).arg(appVersion).arg(system).arg(locale).arg(QT_VERSION_STR);
}
#ifdef Q_OS_MAC
/**
* Only on Mac OS X
* Get the Operating system name and version.
* @return os The operating system name and version in a string.
*/
QString GAnalytics::Private::getSystemInfo()
{
QSysInfo::MacVersion version = QSysInfo::macVersion();
QString os;
switch (version)
{
case QSysInfo::MV_9:
os = "Macintosh; Mac OS 9";
break;
case QSysInfo::MV_10_0:
os = "Macintosh; Mac OS 10.0";
break;
case QSysInfo::MV_10_1:
os = "Macintosh; Mac OS 10.1";
break;
case QSysInfo::MV_10_2:
os = "Macintosh; Mac OS 10.2";
break;
case QSysInfo::MV_10_3:
os = "Macintosh; Mac OS 10.3";
break;
case QSysInfo::MV_10_4:
os = "Macintosh; Mac OS 10.4";
break;
case QSysInfo::MV_10_5:
os = "Macintosh; Mac OS 10.5";
break;
case QSysInfo::MV_10_6:
os = "Macintosh; Mac OS 10.6";
break;
case QSysInfo::MV_10_7:
os = "Macintosh; Mac OS 10.7";
break;
case QSysInfo::MV_10_8:
os = "Macintosh; Mac OS 10.8";
break;
case QSysInfo::MV_10_9:
os = "Macintosh; Mac OS 10.9";
break;
case QSysInfo::MV_10_10:
os = "Macintosh; Mac OS 10.10";
break;
case QSysInfo::MV_10_11:
os = "Macintosh; Mac OS 10.11";
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
case QSysInfo::MV_10_12:
os = "Macintosh; Mac OS 10.12";
break;
#endif
case QSysInfo::MV_Unknown:
os = "Macintosh; Mac OS unknown";
break;
case QSysInfo::MV_IOS_5_0:
os = "iPhone; iOS 5.0";
break;
case QSysInfo::MV_IOS_5_1:
os = "iPhone; iOS 5.1";
break;
case QSysInfo::MV_IOS_6_0:
os = "iPhone; iOS 6.0";
break;
case QSysInfo::MV_IOS_6_1:
os = "iPhone; iOS 6.1";
break;
case QSysInfo::MV_IOS_7_0:
os = "iPhone; iOS 7.0";
break;
case QSysInfo::MV_IOS_7_1:
os = "iPhone; iOS 7.1";
break;
case QSysInfo::MV_IOS_8_0:
os = "iPhone; iOS 8.0";
break;
case QSysInfo::MV_IOS_8_1:
os = "iPhone; iOS 8.1";
break;
case QSysInfo::MV_IOS_8_2:
os = "iPhone; iOS 8.2";
break;
case QSysInfo::MV_IOS_8_3:
os = "iPhone; iOS 8.3";
break;
case QSysInfo::MV_IOS_8_4:
os = "iPhone; iOS 8.4";
break;
case QSysInfo::MV_IOS_9_0:
os = "iPhone; iOS 9.0";
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
case QSysInfo::MV_IOS_9_1:
os = "iPhone; iOS 9.1";
break;
case QSysInfo::MV_IOS_9_2:
os = "iPhone; iOS 9.2";
break;
case QSysInfo::MV_IOS_9_3:
os = "iPhone; iOS 9.3";
break;
case QSysInfo::MV_IOS_10_0:
os = "iPhone; iOS 10.0";
break;
#endif
case QSysInfo::MV_IOS:
os = "iPhone; iOS unknown";
break;
default:
os = "Macintosh";
break;
}
return os;
}
#endif
#ifdef Q_OS_WIN
/**
* Only on Windows
* Get operating system and its version.
* @return os A QString containing the oprating systems name and version.
*/
QString GAnalytics::Private::getSystemInfo()
{
QSysInfo::WinVersion version = QSysInfo::windowsVersion();
QString os("Windows; ");
switch (version)
{
case QSysInfo::WV_95:
os += "Win 95";
break;
case QSysInfo::WV_98:
os += "Win 98";
break;
case QSysInfo::WV_Me:
os += "Win ME";
break;
case QSysInfo::WV_NT:
os += "Win NT";
break;
case QSysInfo::WV_2000:
os += "Win 2000";
break;
case QSysInfo::WV_2003:
os += "Win Server 2003";
break;
case QSysInfo::WV_VISTA:
os += "Win Vista";
break;
case QSysInfo::WV_WINDOWS7:
os += "Win 7";
break;
case QSysInfo::WV_WINDOWS8:
os += "Win 8";
break;
case QSysInfo::WV_WINDOWS8_1:
os += "Win 8.1";
break;
case QSysInfo::WV_WINDOWS10:
os += "Win 10";
break;
default:
os = "Windows; unknown";
break;
}
return os;
}
#endif
#if defined(Q_OS_ANDROID)
#include <QAndroidJniObject>
QString GAnalytics::Private::getSystemInfo()
{
return QString("Linux; U; Android %1; %2 %3 Build/%4; %5")
.arg(QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build$VERSION", "RELEASE").toString())
.arg(QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build", "MANUFACTURER").toString())
.arg(QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build", "MODEL").toString())
.arg(QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build", "ID").toString())
.arg(QAndroidJniObject::getStaticObjectField<jstring>("android/os/Build", "BRAND").toString());
}
#elif defined(Q_OS_LINUX)
#include <sys/utsname.h>
/**
* Only on Unix systems.
* Get operation system name and version.
* @return os A QString with the name and version of the operating system.
*/
QString GAnalytics::Private::getSystemInfo()
{
struct utsname buf;
uname(&buf);
QString system(buf.sysname);
QString release(buf.release);
return system + "; " + release;
}
#endif
/**
* 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> GAnalytics::Private::persistMessageQueue()
{
QList<QString> dataList;
foreach (QueryBuffer buffer, 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 GAnalytics::Private::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;
messageQueue.enqueue(buffer);
}
}
/**
* Change the user id.
* @param userID A string with the user id.
*/
void GAnalytics::Private::setUserID(const QString &userID)
{
this->userID = userID;
QSettings settings;
settings.setValue("GAnalytics-uid", userID);
}
/**
* Get the user id.
* User id once created is stored in application settings.
* @return userID A string with the user id.
*/
QString GAnalytics::Private::getUserID()
{
QSettings settings;
QString userID = settings.value("GAnalytics-uid", QString("")).toString();
return userID;
}
/**
* Get the client id.
* Client id once created is stored in application settings.
* @return clientID A string with the client id.
*/
QString GAnalytics::Private::getClientID()
{
QSettings settings;
QString clientID;
if (!settings.contains("GAnalytics-cid"))
{
clientID = QUuid::createUuid().toString();
settings.setValue("GAnalytics-cid", clientID);
}
else
{
clientID = settings.value("GAnalytics-cid").toString();
}
return clientID;
}
/**
* 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.
* @param query
*/
void GAnalytics::Private::enqueQueryWithCurrentTime(const QUrlQuery &query)
{
QueryBuffer buffer;
buffer.postQuery = query;
buffer.time = QDateTime::currentDateTime();
messageQueue.enqueue(buffer);
}
/**
* Change status of class. Emit signal that status was changed.
* @param doSend
*/
void GAnalytics::Private::setIsSending(bool doSend)
{
if (doSend)
{
timer.stop();
}
else
{
timer.start();
}
bool changed = (isSending != doSend);
isSending = doSend;
if (changed)
{
emit q->isSendingChanged(isSending);
}
}
/**
* CONSTRUCTOR GAnalytics
* ------------------------------------------------------------------------------------------------------------
* Constructs the GAnalytics Object.
* @param parent The application which uses this object.
* @param trackingID
* @param clientID
* @param withGet Determines wheather the messages are send with GET or POST.
*/
GAnalytics::GAnalytics(QObject *parent)
: QObject(parent)
, d(new Private(this))
{
}
GAnalytics::GAnalytics(const QString &trackingID, QObject *parent)
: QObject(parent)
, d(new Private(this))
{
setTrackingID(trackingID);
}
/**
* Destructor of class GAnalytics.
*/
GAnalytics::~GAnalytics()
{
delete d;
}
void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel)
{
if (d->logLevel != logLevel)
{
d->logLevel = logLevel;
emit logLevelChanged();
}
}
GAnalytics::LogLevel GAnalytics::logLevel() const
{
return d->logLevel;
}
// SETTER and GETTER
void GAnalytics::setViewportSize(const QString &viewportSize)
{
if (d->viewportSize != viewportSize)
{
d->viewportSize = viewportSize;
emit viewportSizeChanged();
}
}
QString GAnalytics::viewportSize() const
{
return d->viewportSize;
}
void GAnalytics::setLanguage(const QString &language)
{
if (d->language != language)
{
d->language = language;
emit languageChanged();
}
}
QString GAnalytics::language() const
{
return d->language;
}
void GAnalytics::setTrackingID(const QString &trackingID)
{
if (d->trackingID != trackingID)
{
d->trackingID = trackingID;
emit trackingIDChanged();
}
}
QString GAnalytics::trackingID() const
{
return d->trackingID;
}
void GAnalytics::setSendInterval(int milliseconds)
{
if (d->timer.interval() != milliseconds)
{
d->timer.setInterval(milliseconds);
emit sendIntervalChanged();
}
}
int GAnalytics::sendInterval() const
{
return (d->timer.interval());
}
void GAnalytics::startSending()
{
if (!isSending())
emit d->postNextMessage();
}
bool GAnalytics::isSending() const
{
return d->isSending;
}
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());
}
}
/**
* SentAppview is called when the user changed the applications view.
* Deprecated because after SDK Version 3.08 and up no more "appview" event:
* Use sendScreenView() instead
* @param appName
* @param appVersion
* @param screenName
*/
void GAnalytics::sendAppView(const QString &screenName,
const QVariantMap &customValues)
{
sendScreenView(screenName, customValues);
}
/**
* 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.
* @param appName
* @param appVersion
* @param screenName
*/
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->appName);
query.addQueryItem("av", d->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.
* @param eventCategory
* @param eventAction
* @param eventLabel
* @param eventValue
*/
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->appName);
query.addQueryItem("av", d->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.
* @param exceptionDescription
* @param exceptionFatal
*/
void GAnalytics::sendException(const QString &exceptionDescription,
bool exceptionFatal,
const QVariantMap &customValues)
{
QUrlQuery query = d->buildStandardPostQuery("exception");
query.addQueryItem("an", d->appName);
query.addQueryItem("av", d->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);
}
/**
* 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 GAnalytics::Private::postMessage()
{
if (messageQueue.isEmpty())
{
setIsSending(false);
return;
}
else
{
setIsSending(true);
}
QString connection = "close";
if (messageQueue.count() > 1)
{
connection = "keep-alive";
}
QueryBuffer buffer = messageQueue.head();
QDateTime sendTime = QDateTime::currentDateTime();
qint64 timeDiff = buffer.time.msecsTo(sendTime);
if(timeDiff > fourHours)
{
// too old.
messageQueue.dequeue();
emit postNextMessage();
return;
}
buffer.postQuery.addQueryItem("qt", QString::number(timeDiff));
request.setRawHeader("Connection", connection.toUtf8());
request.setHeader(QNetworkRequest::ContentLengthHeader, buffer.postQuery.toString().length());
// Create a new network access manager if we don't have one yet
if (networkManager == NULL)
{
networkManager = new QNetworkAccessManager(this);
}
QNetworkReply *reply = networkManager->post(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 GAnalytics::Private::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: %s").arg(reply->errorString()));
// An error ocurred.
setIsSending(false);
return;
}
else
{
logMessage(GAnalytics::Debug, "Message sent");
}
messageQueue.dequeue();
emit postNextMessage();
reply->deleteLater();
}
/**
* Qut stream to persist class GAnalytics.
* @param outStream
* @param analytics
* @return
*/
QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics)
{
outStream << analytics.d->persistMessageQueue();
return outStream;
}
/**
* In stream to read GAnalytics from file.
* @param inStream
* @param analytics
* @return
*/
QDataStream &operator >>(QDataStream &inStream, GAnalytics &analytics)
{
QList<QString> dataList;
inStream >> dataList;
analytics.d->readMessagesFromFile(dataList);
return inStream;
}
#ifdef QT_QML_LIB
void GAnalytics::classBegin()
{
// Get the network access manager from the QmlEngine
QQmlContext *context = QQmlEngine::contextForObject(this);
if (context)
{
QQmlEngine *engine = context->engine();
setNetworkAccessManager(engine->networkAccessManager());
}
}
void GAnalytics::componentComplete()
{
}
#endif // QT_QML_LIB
#include "ganalytics.moc"

View File

@ -1,108 +0,0 @@
#ifndef GANALYTICS_H
#define GANALYTICS_H
#include <QObject>
#include <QVariantMap>
#ifdef QT_QML_LIB
#include <QQmlParserStatus>
#endif // QT_QML_LIB
class QNetworkAccessManager;
class GAnalytics : public QObject
#ifdef QT_QML_LIB
, public QQmlParserStatus
#endif // QT_QML_LIB
{
Q_OBJECT
#ifdef QT_QML_LIB
Q_INTERFACES(QQmlParserStatus)
#endif // QT_QML_LIB
Q_ENUMS(LogLevel)
Q_PROPERTY(LogLevel logLevel READ logLevel WRITE setLogLevel NOTIFY logLevelChanged)
Q_PROPERTY(QString viewportSize READ viewportSize WRITE setViewportSize NOTIFY viewportSizeChanged)
Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged)
Q_PROPERTY(QString trackingID READ trackingID WRITE setTrackingID NOTIFY trackingIDChanged)
Q_PROPERTY(int sendInterval READ sendInterval WRITE setSendInterval NOTIFY sendIntervalChanged)
Q_PROPERTY(bool isSending READ isSending NOTIFY isSendingChanged)
public:
explicit GAnalytics(QObject *parent = 0);
explicit GAnalytics(const QString &trackingID, QObject *parent = 0);
~GAnalytics();
public:
enum LogLevel
{
Debug,
Info,
Error
};
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 setTrackingID(const QString &trackingID);
QString trackingID() const;
void setSendInterval(int milliseconds);
int sendInterval() const;
void startSending();
bool isSending() const;
/// 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;
#ifdef QT_QML_LIB
// QQmlParserStatus interface
void classBegin();
void componentComplete();
#endif // QT_QML_LIB
public slots:
void sendScreenView(const QString &screenName,
const QVariantMap &customValues = QVariantMap());
void sendAppView(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();
signals:
void logLevelChanged();
void viewportSizeChanged();
void languageChanged();
void trackingIDChanged();
void sendIntervalChanged();
void isSendingChanged(bool isSending);
private:
class Private;
Private *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);
#endif // GANALYTICS_H

View File

@ -0,0 +1,65 @@
#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, QObject *parent = 0);
~GAnalytics();
public:
enum LogLevel
{
Debug,
Info,
Error
};
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 setSendInterval(int milliseconds);
int sendInterval() const;
void startSending();
bool isSending() const;
/// 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();
signals:
void isSendingChanged(bool isSending);
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);

View File

@ -0,0 +1,222 @@
#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, QObject *parent) : QObject(parent)
{
d = new GAnalyticsWorker(this);
d->m_trackingID = trackingID;
d->m_clientID = clientID;
}
/**
* 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::setSendInterval(int milliseconds)
{
d->m_timer.setInterval(milliseconds);
}
int GAnalytics::sendInterval() const
{
return (d->m_timer.interval());
}
void GAnalytics::startSending()
{
if (!isSending())
d->postMessage();
}
bool GAnalytics::isSending() const
{
return d->m_isSending;
}
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;
}

View File

@ -0,0 +1,252 @@
#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_isSending(false)
{
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.start(30000);
connect(&m_timer, &QTimer::timeout, this, &GAnalyticsWorker::postMessage);
}
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);
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()
{
QString locale = QLocale::system().name();
QString system = Sys::getSystemInfo();
return QString("%1/%2 (%3; %4) GAnalytics/1.0 (Qt/%5)").arg(m_appName).arg(m_appVersion).arg(system).arg(locale).arg(QT_VERSION_STR);
}
/**
* 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);
}
/**
* Change status of class. Emit signal that status was changed.
*/
void GAnalyticsWorker::setIsSending(bool doSend)
{
if (doSend)
{
m_timer.stop();
}
else
{
m_timer.start();
}
bool changed = (m_isSending != doSend);
m_isSending = doSend;
if (changed)
{
emit q->isSendingChanged(m_isSending);
}
}
/**
* 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())
{
setIsSending(false);
return;
}
else
{
setIsSending(true);
}
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());
// 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: %s").arg(reply->errorString()));
// An error ocurred.
setIsSending(false);
return;
}
else
{
logMessage(GAnalytics::Debug, "Message sent");
}
m_messageQueue.dequeue();
postMessage();
reply->deleteLater();
}

View File

@ -0,0 +1,61 @@
#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_isSending;
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);
public slots:
void postMessage();
void postMessageFinished();
};

View File

@ -0,0 +1,11 @@
#pragma once
#include <QString>
namespace Sys
{
/**
* Get operation system name and version.
* @return os A QString with the name and version of the operating system.
*/
QString getSystemInfo();
}

View File

@ -0,0 +1,114 @@
#include "sys.h"
QString Sys::getSystemInfo()
{
QSysInfo::MacVersion version = QSysInfo::macVersion();
QString os;
switch (version)
{
case QSysInfo::MV_9:
os = "Macintosh; Mac OS 9";
break;
case QSysInfo::MV_10_0:
os = "Macintosh; Mac OS 10.0";
break;
case QSysInfo::MV_10_1:
os = "Macintosh; Mac OS 10.1";
break;
case QSysInfo::MV_10_2:
os = "Macintosh; Mac OS 10.2";
break;
case QSysInfo::MV_10_3:
os = "Macintosh; Mac OS 10.3";
break;
case QSysInfo::MV_10_4:
os = "Macintosh; Mac OS 10.4";
break;
case QSysInfo::MV_10_5:
os = "Macintosh; Mac OS 10.5";
break;
case QSysInfo::MV_10_6:
os = "Macintosh; Mac OS 10.6";
break;
case QSysInfo::MV_10_7:
os = "Macintosh; Mac OS 10.7";
break;
case QSysInfo::MV_10_8:
os = "Macintosh; Mac OS 10.8";
break;
case QSysInfo::MV_10_9:
os = "Macintosh; Mac OS 10.9";
break;
case QSysInfo::MV_10_10:
os = "Macintosh; Mac OS 10.10";
break;
case QSysInfo::MV_10_11:
os = "Macintosh; Mac OS 10.11";
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
case QSysInfo::MV_10_12:
os = "Macintosh; Mac OS 10.12";
break;
#endif
case QSysInfo::MV_Unknown:
os = "Macintosh; Mac OS unknown";
break;
case QSysInfo::MV_IOS_5_0:
os = "iPhone; iOS 5.0";
break;
case QSysInfo::MV_IOS_5_1:
os = "iPhone; iOS 5.1";
break;
case QSysInfo::MV_IOS_6_0:
os = "iPhone; iOS 6.0";
break;
case QSysInfo::MV_IOS_6_1:
os = "iPhone; iOS 6.1";
break;
case QSysInfo::MV_IOS_7_0:
os = "iPhone; iOS 7.0";
break;
case QSysInfo::MV_IOS_7_1:
os = "iPhone; iOS 7.1";
break;
case QSysInfo::MV_IOS_8_0:
os = "iPhone; iOS 8.0";
break;
case QSysInfo::MV_IOS_8_1:
os = "iPhone; iOS 8.1";
break;
case QSysInfo::MV_IOS_8_2:
os = "iPhone; iOS 8.2";
break;
case QSysInfo::MV_IOS_8_3:
os = "iPhone; iOS 8.3";
break;
case QSysInfo::MV_IOS_8_4:
os = "iPhone; iOS 8.4";
break;
case QSysInfo::MV_IOS_9_0:
os = "iPhone; iOS 9.0";
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
case QSysInfo::MV_IOS_9_1:
os = "iPhone; iOS 9.1";
break;
case QSysInfo::MV_IOS_9_2:
os = "iPhone; iOS 9.2";
break;
case QSysInfo::MV_IOS_9_3:
os = "iPhone; iOS 9.3";
break;
case QSysInfo::MV_IOS_10_0:
os = "iPhone; iOS 10.0";
break;
#endif
case QSysInfo::MV_IOS:
os = "iPhone; iOS unknown";
break;
default:
os = "Macintosh";
break;
}
return os;
}

View File

@ -0,0 +1,13 @@
#include "sys.h"
#include <sys/utsname.h>
QString Sys::getSystemInfo()
{
struct utsname buf;
uname(&buf);
QString system(buf.sysname);
QString release(buf.release);
return system + "; " + release;
}

View File

@ -0,0 +1,50 @@
#include "sys.h"
QString Sys::getSystemInfo()
{
QSysInfo::WinVersion version = QSysInfo::windowsVersion();
QString os("Windows; ");
switch (version)
{
case QSysInfo::WV_95:
os += "Win 95";
break;
case QSysInfo::WV_98:
os += "Win 98";
break;
case QSysInfo::WV_Me:
os += "Win ME";
break;
case QSysInfo::WV_NT:
os += "Win NT";
break;
case QSysInfo::WV_2000:
os += "Win 2000";
break;
case QSysInfo::WV_2003:
os += "Win Server 2003";
break;
case QSysInfo::WV_VISTA:
os += "Win Vista";
break;
case QSysInfo::WV_WINDOWS7:
os += "Win 7";
break;
case QSysInfo::WV_WINDOWS8:
os += "Win 8";
break;
case QSysInfo::WV_WINDOWS8_1:
os += "Win 8.1";
break;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
case QSysInfo::WV_WINDOWS10:
os += "Win 10";
break;
#endif
default:
os = "Windows; unknown";
break;
}
return os;
}