在第一篇文章中,重點介紹了腳本需要搜集的數據,而本篇主要介紹的是服務器端如何處理客戶端發送過來的請求和參數。
一、設備信息檢測
通過分析User-Agent請求首部(如下圖紅線框出的部分),可以得到相關的設備信息。
Piwik系統專門有一套代碼用來分析代理信息,還獨立了出來,叫做DeviceDetector。它有一個專門的demo頁面,可以展示其功能,點進去后可以看到下圖中的內容。
它能檢測出瀏覽器名稱、瀏覽器的渲染引擎、瀏覽器的版本、設備品牌(例如HTC、Apple、HP等)、設備型號(例如iPad、Nexus 5、Galaxy S5等)、設備類別(例如desktop、smartphone、tablet等),這6類數據中的可供選擇的關鍵字,可以參考“List of segments”或插件的“readme”。順便說一下,Piwik還能獲取到訪客的定位信息,在“List of segments”中,列舉出了城市、經緯度等信息,其原理暫時還沒研究。
Piwik為大部分設備信息的關鍵字配備了一個icon圖標,所有的icon圖標被放置在“plugins\Morpheus\icons”中,包括瀏覽器、設備、國旗、操作系統等,下圖截取的是瀏覽器中的部分圖標。
二、IP地址
在Piwik系統的后台設置中,可以選擇IP地址的獲取方式(如下圖所示)。在官方博客的一篇《Geo Locate your visitors》博文中提到,3.5版本后可以在系統中嵌入MaxMind公司提供的IP地理定位服務(GeoIP2)。
下面是一張看官方的產品介紹表,從描述中可看出這是一項非常厲害的服務。不過需要注意的是,這是一項付費服務。
三、日志數據和歸檔數據
在官方發布的說明文檔《How Matomo (formerly Piwik) Works》中提到,在Piwik中有兩種數據類型:日志數據和歸檔數據。日志數據(Log Data)是一種原始分析數據,從客戶端發送過來的參數就是日志數據,剛剛設備檢測到的信息也是日志數據,還有其它的一些日志數據的來源,暫時還沒細究。由於日志數據非常巨大,因此不能直接生成最終用戶可看的報告,得使用歸檔數據來生成報告。歸檔數據(Archive Data)是以日志數據為基礎而構建出來的,它是一種被緩存並且可用於生成報告的聚合分析數據。
日志數據會通過“core\Piwik\Tracker\Visit.php”中的方法保存到數據庫中,其中核心的方法如下所示,注釋中也強調了該方法中的內容是處理請求的主要邏輯。該方法涉及到了很多對象,以及對象的方法,錯綜復雜,我自己也沒有研究透,只是利用PHPStorm編輯器自動索引,查找出了一些關聯,具體細節還有待考證。
/** * Main algorithm to handle the visit. * * Once we have the visitor information, we have to determine if the visit is a new or a known visit. * * 1) When the last action was done more than 30min ago, * or if the visitor is new, then this is a new visit. * * 2) If the last action is less than 30min ago, then the same visit is going on. * Because the visit goes on, we can get the time spent during the last action. * * NB: * - In the case of a new visit, then the time spent * during the last action of the previous visit is unknown. * * - In the case of a new visit but with a known visitor, * we can set the 'returning visitor' flag. * * In all the cases we set a cookie to the visitor with the new information. */ public function handle() { foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::manipulateRequest()..."); $processor->manipulateRequest($this->request); } $this->visitProperties = new VisitProperties(); foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::processRequestParams()..."); $abort = $processor->processRequestParams($this->visitProperties, $this->request); if ($abort) { Common::printDebug("-> aborting due to processRequestParams method"); return; } } $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); if (!$isNewVisit) { $isNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions() , 'shouldForceNewVisit'); $this->request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit); } foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::afterRequestProcessed()..."); $abort = $processor->afterRequestProcessed($this->visitProperties, $this->request); if ($abort) { Common::printDebug("-> aborting due to afterRequestProcessed method"); return; } } $isNewVisit = $this->request->getMetadata('CoreHome', 'isNewVisit'); // Known visit when: // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor // OR // - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor() // ) // AND // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() if (!$isNewVisit) { try { $this->handleExistingVisit($this->request->getMetadata('Goals', 'visitIsConverted')); } catch(VisitorNotFoundInDb $e) { $this->request->setMetadata('CoreHome', 'visitorNotFoundInDb', true); // TODO: perhaps we should just abort here? } } // New visit when: // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit() // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor() // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB if ($isNewVisit) { $this->handleNewVisit($this->request->getMetadata('Goals', 'visitIsConverted')); } // update the cookie with the new visit information $this->request->setThirdPartyCookie($this->request->getVisitorIdForThirdPartyCookie()); foreach ($this->requestProcessors as $processor) { Common::printDebug("Executing " . get_class($processor) . "::recordLogs()..."); $processor->recordLogs($this->visitProperties, $this->request); } $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished(); }
最后了解一下Piwik的數據庫設計,此處只分析與日志數據和歸檔數據有關的數據表。官方的說明文檔曾介紹,日志數據有5張相關的數據表,我對於表的內在含義還比較模糊,因此下面所列的描述還不是很清晰。
(1)log_visit:每次訪問都會生成一條訪問者記錄,表中的字段可參考“Visits”。
(2)log_action:網站上的訪問和操作類型(例如特定URL、網頁標題),可分析出訪問者感興趣的頁面,表中的字段可參考“Action Types”。
(3)log_link_visit_action:訪問者在瀏覽期間執行的操作,表中的字段可參考“Visit Actions”。
(4)log_conversion:訪問期間發生的轉化(與目標相符的操作),表中的字段可參考“Conversions”。
(5)log_conversion_item:與電子商務相關的信息,表中的字段可參考“Ecommerce items”。
歸檔數據的表有兩種前綴,分別是“archive_numeric_”和“archive_blob_”,表的字段可參考“Archive data”。通過對字段的觀察可知,兩種最大的不同就是value字段的數據類型。archive_numeric_* 表中的value能儲存數值(數據類型是Double),而archive_blob_* 表中的value能儲存出數字以外的其他任何數據(數據類型是Blob)。
兩種表都是動態生成的,因此前綴的后面都用“*”表示。生成規則可按年、月、周、天或自定義日期范圍,不設置的話,默認是按月計算,例如archive_numeric_2018_09、archive_blob_2018_09。
參考資料:
How Matomo (formerly Piwik) Works