1 /* 2 3 Copyright (c) 2014, Michael Tesch 4 All rights reserved. 5 6 Redistribution and use in source and binary forms, with or without 7 modification, are permitted provided that the following conditions are met: 8 9 * Redistributions of source code must retain the above copyright notice, this 10 list of conditions and the following disclaimer. 11 12 * Redistributions in binary form must reproduce the above copyright notice, 13 this list of conditions and the following disclaimer in the documentation 14 and/or other materials provided with the distribution. 15 16 * Neither the name of Michael Tesch nor the names of other 17 contributors may be used to endorse or promote products derived from 18 this software without specific prior written permission. 19 20 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31 https://github.com/tesch1/qt-google-analytics-collector 32 33 to enable debugging messages, '#define GANALYTICS_DEBUG 1' before including this file 34 to get super verbose debugging, '#define GANALYTICS_DEBUG 2' 35 36 To build GAnalytics with QtQuick application (QGuiApplication) instead of Desktop, 37 define GANALYTICS_QTQUICK in your .pro file like this: 'DEFINES += GANALYTICS_QTQUICK', 38 or in cmake project: 'add_definitions(-DGANALYTICS_QTQUICK)' 39 */ 40 #include <QCoreApplication> 41 #include <QSettings> 42 43 #if defined(GANALYTICS_QTQUICK) 44 #include <QGuiApplication> 45 #include <QScreen> 46 #else 47 #ifdef QT_GUI_LIB 48 #include <QApplication> 49 #include <QDesktopWidget> 50 #endif 51 #endif 52 53 #include <QUuid> 54 #include <QNetworkAccessManager> 55 #include <QNetworkRequest> 56 #include <QNetworkReply> 57 #include <QUrl> 58 #include <QDebug> 59 #include <QProcess> 60 #include <QList> 61 #include <QEventLoop> 62 63 #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 64 #include <QUrlQuery> 65 #define URL_QUERY QUrlQuery 66 #else 67 #define URL_QUERY QUrl 68 #endif 69 70 #include "common.h" 71 72 /*! 73 * send google tracking data according to 74 * https://developers.google.com/analytics/devguides/collection/protocol/v1/reference 75 */ 76 77 #ifndef GANALYTICS_DEBUG 78 #define GANALYTICS_DEBUG 0 79 #endif 80 81 class GAnalytics : public QObject { 82 Q_OBJECT Q_PROPERTY(bool isBusy READ isBusy NOTIFY busyChanged)83 Q_PROPERTY (bool isBusy READ isBusy NOTIFY busyChanged) 84 public: 85 GAnalytics(QCoreApplication * parent, 86 QString trackingID, 87 QString clientID = "", 88 bool useGET = false) 89 : QObject(parent), _qnam(this), _trackingID(trackingID), _clientID(clientID), _useGET(useGET), _isFail(false) 90 , _waitLoop() 91 { 92 if (parent) { 93 setAppName(parent->applicationName()); 94 setAppVersion(parent->applicationVersion()); 95 #ifdef QT_GUI_LIB 96 parent->dumpObjectTree(); 97 #endif 98 } 99 if (!_clientID.size()) { 100 // load client id from settings 101 QSettings settings; 102 if (!settings.contains("GAnalytics-cid")) { 103 settings.setValue("GAnalytics-cid", QUuid::createUuid().toString()); 104 } 105 _clientID = settings.value("GAnalytics-cid").toString(); 106 } 107 connect(&_qnam, SIGNAL(finished(QNetworkReply *)), this, SLOT(replyFinished(QNetworkReply *))); 108 #if QT_VERSION >= 0x040800 109 #if GANALYTICS_DEBUG 110 if (!_qnam.networkAccessible()) 111 qDebug() << "error: network inaccessible\n"; 112 #endif 113 #endif 114 } 115 ~GAnalytics()116 ~GAnalytics() { 117 // this generally happens after the event-loop is done, so no more network processing 118 #if GANALYTICS_DEBUG 119 QList<QNetworkReply *> replies = _qnam.findChildren<QNetworkReply *>(); 120 for (QList<QNetworkReply *>::iterator it = replies.begin(); it != replies.end(); it++) { 121 if ((*it)->isRunning()) 122 qDebug() << "~GAnalytics, request still running: " << (*it)->url().toString() << ", aborting."; 123 //reply->deleteLater(); 124 } 125 qWarning() << "~GAnalytics"; 126 #endif 127 } 128 129 // manual config of static fields setClientID(QString clientID)130 void setClientID(QString clientID) { _clientID = clientID; } setUserID(QString userID)131 void setUserID(QString userID) { _userID = userID; } setUserIPAddr(QString userIPAddr)132 void setUserIPAddr(QString userIPAddr) { _userIPAddr = userIPAddr; } setUserAgent(QString userAgent)133 void setUserAgent(QString userAgent) { _userAgent = userAgent; } setAppName(QString appName)134 void setAppName(QString appName) { _appName = appName; } setAppVersion(QString appVersion)135 void setAppVersion(QString appVersion) { _appVersion = appVersion; } setScreenResolution(QString screenResolution)136 void setScreenResolution(QString screenResolution) { _screenResolution = screenResolution; } setViewportSize(QString viewportSize)137 void setViewportSize(QString viewportSize) { _viewportSize = viewportSize; } setUserLanguage(QString userLanguage)138 void setUserLanguage(QString userLanguage) { _userLanguage = userLanguage; } getClientID()139 QString getClientID() const { return _clientID; } getUserAgent()140 QString getUserAgent() const { return _userAgent; } 141 142 // 143 // hit types 144 // 145 // - https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide 146 // 147 148 // query processing in progress isBusy()149 bool isBusy() const { return !_workingQueries.isEmpty(); } 150 151 public Q_SLOTS: 152 153 // pageview sendPageview(QString docHostname,QString page,QString title)154 void sendPageview(QString docHostname, QString page, QString title) { 155 URL_QUERY params = build_metric("pageview"); 156 params.addQueryItem("dh", docHostname); // document hostname 157 params.addQueryItem("dp", page); // page 158 params.addQueryItem("dt", title); // title 159 send_metric(params); 160 } 161 162 // event 163 void sendEvent(const QString& eventCategory, 164 const QString& eventAction, 165 const QString& eventLabel = QString(), 166 int eventValue = 0) { 167 URL_QUERY params = build_metric("event"); 168 if (_appName.size()) params.addQueryItem("an", _appName); // mobile event app tracking 169 if (_appVersion.size()) params.addQueryItem("av", _appVersion); // mobile event app tracking 170 if (eventCategory.size()) params.addQueryItem("ec", eventCategory); 171 if (eventAction.size()) params.addQueryItem("ea", eventAction); 172 if (eventLabel.size()) params.addQueryItem("el", eventLabel); 173 if (eventValue) params.addQueryItem("ev", QString::number(eventValue)); 174 send_metric(params); 175 } 176 177 // transaction 178 void sendTransaction(QString transactionID, QString transactionAffiliation = "" /*...todo...*/) { 179 URL_QUERY params = build_metric("transaction"); 180 params.addQueryItem("ti", transactionID); 181 if (transactionAffiliation.size()) params.addQueryItem("ta", transactionAffiliation); 182 send_metric(params); 183 } 184 185 // item sendItem(QString itemName)186 void sendItem(QString itemName) { 187 URL_QUERY params = build_metric("item"); 188 params.addQueryItem("in", itemName); 189 //if (appName.size()) params.addQueryItem("an", appName); 190 send_metric(params); 191 } 192 193 // social sendSocial(QString socialNetwork,QString socialAction,QString socialActionTarget)194 void sendSocial(QString socialNetwork, QString socialAction, QString socialActionTarget) { 195 URL_QUERY params = build_metric("social"); 196 params.addQueryItem("sn", socialNetwork); 197 params.addQueryItem("sa", socialAction); 198 params.addQueryItem("st", socialActionTarget); 199 send_metric(params); 200 } 201 202 // exception 203 void sendException(QString exceptionDescription, bool exceptionFatal = true) { 204 URL_QUERY params = build_metric("exception"); 205 if (exceptionDescription.size()) params.addQueryItem("exd", exceptionDescription); 206 if (!exceptionFatal) params.addQueryItem("exf", "0"); 207 send_metric(params); 208 } 209 210 // timing sendTiming()211 void sendTiming(/* todo */) { 212 URL_QUERY params = build_metric("timing"); 213 //if (appName.size()) params.addQueryItem("an", appName); 214 send_metric(params); 215 } 216 217 // screenview 218 void sendScreenview(const QString& screenName, const QString& appName = QString(), const QString& appVersion = QString()) { 219 URL_QUERY params = build_metric("screenview"); 220 if (!appName.isEmpty()) params.addQueryItem("an", appName); 221 else if (!_appName.isEmpty()) params.addQueryItem("an", _appName); 222 if (!appVersion.isEmpty()) params.addQueryItem("av", appVersion); 223 else if (!_appVersion.isEmpty()) params.addQueryItem("av", _appVersion); 224 if (screenName.size()) params.addQueryItem("cd", screenName); 225 send_metric(params); 226 } 227 228 // custom dimensions / metrics 229 // todo 230 231 // experiment id / variant 232 // todo 233 234 //void startSession(); startSession()235 void startSession() { 236 URL_QUERY params = build_metric("event"); 237 if (_appName.size()) params.addQueryItem("an", _appName); // mobile event app tracking 238 if (_appVersion.size()) params.addQueryItem("av", _appVersion); // mobile event app tracking 239 params.addQueryItem("sc", "start"); 240 params.addQueryItem("ec", "General"); 241 params.addQueryItem("ea", "Start"); 242 params.addQueryItem("ev", "0"); 243 send_metric(params); 244 } 245 246 // To ensure that query was sent before application quit, call waitForIdle() endSession()247 void endSession() { 248 URL_QUERY params = build_metric("event"); 249 if (_appName.size()) params.addQueryItem("an", _appName); // mobile event app tracking 250 if (_appVersion.size()) params.addQueryItem("av", _appVersion); // mobile event app tracking 251 params.addQueryItem("sc", "end"); 252 params.addQueryItem("ec", "General"); 253 params.addQueryItem("ea", "End"); 254 params.addQueryItem("ev", "0"); 255 send_metric(params); 256 } 257 258 // Waiting for any network operations complete. This method can be used with endSession 259 // to ensure that query was completed before application was closed. waitForIdle()260 void waitForIdle() { 261 if (_waitLoop) { 262 qCritical() << "Recursive call GAnalytics::waitForIdle"; 263 return; 264 } 265 266 QEventLoop loop; 267 _waitLoop = &loop; 268 loop.exec(QEventLoop::ExcludeUserInputEvents); 269 _waitLoop = 0; 270 } 271 272 signals: 273 void busyChanged(); 274 275 public: 276 generateUserAgentEtc()277 void generateUserAgentEtc() { 278 QString locale = QLocale::system().name().toLower().replace("_", "-"); 279 QString os = Common::operatingSystemLong(); 280 _userAgent = "Mozilla/5.0 (" + os + "; " + locale + ") GAnalytics/1.0 (Qt/" QT_VERSION_STR ")"; 281 _userLanguage = locale; 282 #if defined(GANALYTICS_QTQUICK) 283 QScreen* screen = qApp->primaryScreen(); 284 QString geom = QString::number(screen->geometry().width()) 285 + "x" + QString::number(screen->geometry().height()); 286 _screenResolution = geom; 287 #else 288 #ifdef QT_GUI_LIB 289 QString geom = QString::number(QApplication::desktop()->screenGeometry().width()) 290 + "x" + QString::number(QApplication::desktop()->screenGeometry().height()); 291 _screenResolution = geom; 292 #endif 293 #endif 294 #if GANALYTICS_DEBUG > 1 295 qDebug() << "User-Agent:" << _userAgent; 296 qDebug() << "Language:" << _userLanguage; 297 qDebug() << "Screen Resolution:" << _screenResolution; 298 #endif 299 } 300 301 private Q_SLOTS: 302 replyFinished(QNetworkReply * reply)303 void replyFinished(QNetworkReply * reply) { 304 //qDebug() << "reply:" << reply->readAll().toHex(); 305 if (QNetworkReply::NoError != reply->error()) { 306 qCritical() << "replyFinished error: " << reply->errorString() << "\n"; 307 } 308 else { 309 int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 310 //qDebug() << "response code: " << httpStatus; 311 if (httpStatus < 200 || httpStatus > 299) { 312 #if GANALYTICS_DEBUG 313 qDebug() << "response code: " << httpStatus; 314 #endif 315 _isFail = true; 316 } 317 } 318 319 _workingQueries.removeAll(reply); 320 if (_workingQueries.isEmpty()) { 321 emit busyChanged(); 322 if (_waitLoop) 323 _waitLoop->exit(); 324 } 325 326 reply->deleteLater(); 327 } 328 replyError(QNetworkReply::NetworkError code)329 void replyError(QNetworkReply::NetworkError code) { 330 qDebug() << "network error signal: " << code << "\n"; 331 } 332 333 private: 334 GAnalytics(const GAnalytics &); // disable copy const constructor build_metric(const QString & hitType)335 URL_QUERY build_metric(const QString& hitType) const { 336 URL_QUERY params; 337 // required in v1 338 params.addQueryItem("v", "1" ); // version 339 params.addQueryItem("tid", _trackingID); 340 params.addQueryItem("cid", _clientID); 341 params.addQueryItem("t", hitType); 342 // optional 343 if (_userID.size()) 344 params.addQueryItem("uid", _userID); 345 if (_userIPAddr.size()) 346 params.addQueryItem("uip", _userIPAddr); 347 if (_screenResolution.size()) 348 params.addQueryItem("sr", _screenResolution); 349 if (_viewportSize.size()) 350 params.addQueryItem("vp", _viewportSize); 351 if (_userLanguage.size()) 352 params.addQueryItem("ul", _userLanguage); 353 //if (_userAgent.size()) 354 // params.addQueryItem("ua", _userAgent); 355 return params; 356 } 357 send_metric(const URL_QUERY & params)358 void send_metric(const URL_QUERY & params) { 359 // when google has err'd us, then stop sending events! 360 if (_isFail) 361 return; 362 QUrl collect_url("http://www.google-analytics.com/collect"); 363 QNetworkRequest request; 364 if (_userAgent.size()) 365 request.setRawHeader("User-Agent", _userAgent.toUtf8()); 366 QNetworkReply * reply; 367 if (_useGET) { 368 // add cache-buster "z" to params 369 //params.addQueryItem("z", QString::number(qrand()) ); 370 #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 371 collect_url.setQuery(params); 372 #else 373 collect_url.setQueryItems(params.queryItems()); 374 #endif 375 request.setUrl(collect_url); 376 reply = _qnam.get(request); 377 } 378 else { 379 request.setUrl(collect_url); 380 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); 381 382 #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) 383 QByteArray postData = params.query(QUrl::FullyEncoded).toUtf8(); 384 #else 385 QByteArray postData = params.encodedQuery(); 386 #endif 387 reply = _qnam.post(request, postData); 388 } 389 _workingQueries << reply; 390 if (_workingQueries.size() == 1) 391 emit busyChanged(); 392 393 connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), 394 this, SLOT(replyError(QNetworkReply::NetworkError))); 395 #if GANALYTICS_DEBUG > 1 396 qDebug() << "GAnalytics sent: " << params.toString(); 397 #endif 398 reply->setParent(&_qnam); 399 } 400 mutable QNetworkAccessManager _qnam; 401 QString _trackingID; 402 QString _clientID; 403 bool _useGET; // true=GET, false=POST 404 QString _userID; 405 406 // various parameters: 407 bool _anonymizeIP; 408 bool _cacheBust; 409 // 410 QString _userIPAddr; 411 QString _userAgent; 412 QString _appName; 413 QString _appVersion; 414 #if 0 // todo 415 // traffic sources 416 QString _documentReferrer; 417 QString _campaignName; 418 QString _campaignSource; 419 QString _campaignMedium; 420 QString _campaignKeyword; 421 QString _campaignContent; 422 QString _campaignID; 423 QString _adwordsID; 424 QString _displayAdsID; 425 #endif 426 // system info 427 QString _screenResolution; 428 QString _viewportSize; 429 QString _userLanguage; 430 // etc... 431 432 // internal 433 bool _isFail; 434 435 QList<QNetworkReply*> _workingQueries; 436 QEventLoop* _waitLoop; 437 }; 438 439