Hexo Icarus 顯示瀏覽量(二):廉價的 Solution,Google Analytics + AWS Lambda

既然都選用 Hexo 作為部落格的框架了,除了因為全部皆為靜態網頁能達到的 High Performance 外,最大的優點想必就是價格了。

當然這是針對維護一個中小型部落格的「價格量級」來做比較的,實際上架設一座小型的 Word Press 部落格,可能最多也就每月幾百元新台幣的價格,和接下來要提到趨近免費的 Solution 來說,在成本上的差異主要就只是每個月能少噴幾百元,個人感受比較好罷了。

接下來便來聊聊這個 Solution:透過 Google Analytics 來儲存瀏覽量、AWS Lambda 提供 API 獲取瀏覽量。

瀏覽量的定義及儲存,為何選擇 Google Analytics?

上一講我們提到記錄文章瀏覽的次數需要一座資料庫,可以在每次有人瀏覽文章的時候便在資料庫中多記一筆瀏覽數目,便能夠得到一篇文章被閱讀多少次。

然而,這種記錄方式要達到準確的判斷「瀏覽量」,其實是需要花不少功夫的。首先會遇到的問題就是你要怎麼定義「瀏覽量」?

  • 每次進入頁面都算一次瀏覽嗎?那麼我不斷的重新整理頁面是否就能讓瀏覽量快速增長?

  • 承上,為了避免重新整理能快速提升瀏覽量,是否要讓同一使用者在一段時間內的瀏覽都只算 1 次?是的話「一段時間」又是多久?

  • 開 3 個 Tabs 或 Windows 瀏覽同個頁面,要計算成 1 次還是 3 次?

諸如此類的問題,如果要自己設計和實作邏輯來計算瀏覽量,真的是有點麻煩。

因此,使用第三方的服務就成為我的首選了,Google Analytics 更是免費、好整合,功能又完整的統計工具,除了幫我統計某頁面的瀏覽數之外,也能統計瀏覽者的來源、使用的設備等等其他有用資訊。

只要申請 Google Analytics 帳號之後,將長得像是 G-XXXXXXXXXX 的 Tracking ID 填入 Hexo 設定檔中即可使用。

使用 Google Analytics Data API 獲取瀏覽量資訊

取得 Google Analytics 的帳號後,我們接下來需要 Google Analytics Data API 的使用權限,可以參照最後附錄的部分在 Google Cloud 申請服務。

再來我們可以透過 Google Analytics 提供在 NPM 的 Library 來使用其服務,以下範例是用 Node.js 寫的一段程式碼。

1
2
3
4
5
6
7
8
9
10
11
const analyticsData = google.analyticsdata('v1beta');
await analyticsData.properties.runReport({
auth: jwt,
property: `properties/${propertyId}`,
requestBody: {
dateRanges: [{ startDate: '2019-01-01', endDate: 'today' }],
metrics: [{ name: 'screenPageViews' }],
dimensions: [{ name: 'pagePath' }],
dimensionFilter: { filter: { fieldName: 'pagePath', inListFilter: { values: pagePathList } } },
},
});

要使用這個 API,需要 jwtpropertyId。其中 jwt 內容的來源是在 Google Cloud 申請完 Data API 服務後,透過下載下來的 service account private key 所產生的 token;而 propertyId 也是在申請 Data API 服務最後可以取得的資訊。

有了與身份和權限相關的資訊,就能使用 Data API 來獲取瀏覽量了。在這段 Snippet 中,我們所 Request 的 Metric 名稱為 screenPageViews,後續的 dimensionsdimensionFilter 則表示每個 page view 以頁面路徑(pagePath)來顯示,藉由傳入一組 pagePathList 來取得這個 Array 中的頁面與其對應的瀏覽數。

最後得到的資訊就會長得像是:

1
2
3
4
{
"/A/": "1234",
"/B/": "4321"
}

使用 AWS Lambda 提供 Public API

在透過 Google Analytics Data API 取得頁面瀏覽數的同時,由於我們會附帶一些較為機密的 jwtpropertyId,尤其產生 jwt 的過程中需要使用一組私鑰,因此這段程式碼不太適合直接寫在 Hexo(也就是放在前端的程式碼中)。

所以我們就要自己寫一支 API 把拿頁面的機密資訊藏起來,僅僅提供透過頁面路徑來查詢頁面瀏覽數的功能,此 API 就可以使用 AWS Lambda 這個服務來實作。

將私鑰隱藏在 AWS Lambda 中

會選擇 AWS Lambda 的主因也是價格,當然也有個人偏好。我自己比較熟悉 AWS 的服務,所以雖然 Google Cloud 也有提供價格和功能類似的 Google Cloud Functions,最後還是挑了 AWS Lambda。

作為 Serverless 的運算服務,在價格方面,AWS Lambda 就算不使用 Free Tier 的方案,每 100 萬次 Request 也只要 0.2 美元左右,對於讀者不多的小型部落格,估算使用量後可以說是趨近免費。

以下提供寫在 AWS Lambda 中的完整程式碼,使用 Node.js 來開發:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { google } from 'googleapis';

const analyticsData = google.analyticsdata('v1beta');
const key = JSON.parse(process.env.GOOGLE_CREDENTIALS);
const jwt = new google.auth.JWT(key.client_email, null, key.private_key, ['https://www.googleapis.com/auth/analytics.readonly']);

export const handler = async (event) => {
const allowedOrigins = ['http://localhost:4000', 'https://www.howardsnotes.tw'];
const origin = event.headers.origin || event.headers.referer || '';
const allowOrigin = allowedOrigins.some((allowed) => origin.startsWith(allowed)) ? origin : '';
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': allowOrigin,
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (event.requestContext.http.method === 'OPTIONS') return { statusCode: 200, headers };

// POST Method
const pagePathList = JSON.parse(event.body).pagePathList;
await jwt.authorize();
const response = await analyticsData.properties.runReport({
auth: jwt,
property: 'properties/{PROPERTY_ID}', // Replace {PROPERTY_ID}
requestBody: {
dateRanges: [{ startDate: '2019-01-01', endDate: 'today' }],
metrics: [{ name: 'screenPageViews' }],
dimensions: [{ name: 'pagePath' }],
dimensionFilter: { filter: { fieldName: 'pagePath', inListFilter: { values: pagePathList } } },
},
});
const rows = response.data.rows;
const pageCountMap = rows.reduce((pageCountMap, row) => {
pageCountMap[row.dimensionValues[0].value] = row.metricValues[0].value;
return pageCountMap;
}, {});
return { statusCode: 200, headers, body: { pageCountMap } };
};

裡面還是花了一些篇幅來處理開發時和實際部署的 CORS 的問題,這裡就特別不贅述了。

總而言之,透過 AWS Lambda 實作出的 Public API,便能在之後整合到 Hexo 的頁面中,取得頁面的瀏覽量。

此方案之優缺點

在選擇 Google Analytics 加上 AWS Lambda 時,兩者的共通點就是價格相當低。但所謂天下沒有白吃的午餐,要使用價格低廉的服務就勢必得付出一些代價。

在這個方案中的最大缺點就是效能蠻差的,更具體一點,是執行的速度很緩慢。透過 Google Analytics Data API 取得頁面瀏覽數在我的體感上就大致需要 1-3 秒;而 AWS Lambda 的效能則更差,最差的狀況之下,需要等待長達 10 秒。

這是由於 Serverless 的特性,在長時間沒有使用的情況下,AWS Lambda 會把資源分配出去,所以在下次的 Request 時,API 會進入 Cold Start 的狀態,要等待資源被重新被取得,才能繼續「快速」的被使用。

但,雖然有 Cold Start 造成的不便,AWS 大致還算是穩定的。而且瀏覽數並非一篇文章中的主要重點,我們只要避免 Request 瀏覽數緩慢或失敗的這件事,不要影響到文章內容的閱讀即可。

相對於價格低廉,這個缺點我還算是能夠忍受!

附錄:申請 Google Analytics Data API

首先,需要已經在 Google Analytics 有了帳號,才能到 Google Cloud 申請 Google Analytics Data API 並綁定 Google Analytics 的帳號。

在 Google Cloud 底下新增一個 Project。

新增 Google Cloud Project

然後 Enable 此 Project 底下的 Google Analytics Data API。

Enable Google Analytics Data API

接著,由於需要設定誰能夠使用此 Data API,我們可以創建一個 Service Account,透過此 Account 來使用 Data API。

創建 Service Account

輸入 Service Account 資訊

確認創建 Service Account

創建完畢 Service Account 之後,我們需要一組私鑰(Private Key),用以建立之後呼叫 API 所攜帶的 JWT 這個 Token。

先建立一組新的 Key

然後將此 Key 以 JSON 形式下載

下載完畢之後要妥善保存,這份檔案在上面 AWS Lambda 的 Code 中會引用(存在環境變數中被取用)。

然後便可以進行和 Google Analytics 建立連結的動作了。現在需要進入 Google Analytics 的控制介面(非 Google Cloud),將剛剛在 Google Cloud Project 中建立 Service Account 時產生的 Email 貼到 Google Analytics 控制介面中。

將 Service Account Email 貼入 Google Analytics 建立連結

最後便能在 Google Analytics 中找到一組 Property ID,也會用在上面 AWS Lambda 的 Code 當中。

Property ID

附錄:AWS Lambda 介面操作

這邊記錄 AWS Lambda 透過 Console 將寫好的 Code 丟入後,測試、執行,首先先建立一個 Function,我們這邊命名為 blog-page-view

進入 AWS Lambda Console,建立 Function

進入 Configuration 底下的 Environment variables,這裡主要需要儲存的就是上面 Google Analytics Data API 附錄中,我們所下載的那份 JSON 格式的 Key。

以環境變數的方式儲存會比直接以檔案的方式放在 Lambda 中來得安全。

Environment variables 介面

我們建立一個叫做 GOOGLE_CREDENTIALS 的環境變數,並且將整份 JSON 檔案的內容都丟到 Value 當中。之後我們便能在 Code 裡面以這樣的方式取得整組 Key:const key = JSON.parse(process.env.GOOGLE_CREDENTIALS);

將 Key 放入環境變數

然後將我們的 Code 貼到 AWS Lambda 中,以及我們需要將 package.json 以及 node_modules 整包上傳,因為尚不支援在 Lambda 的介面中安裝套件。所以需要注意的是我們在本機端安裝的套件需要和 AWS Lambda 的機器相容,舉例來說我使用蘋果的 M 系列 ARM 筆電,就要注意 AWS Lambda 機型不要選到 x86 架構的。

AWS Lambda 編輯程式碼

都設定完畢之後,就可以開始測試和部署了,我們可以用 AWS Lambda 的測試來輸入某個 Page Path,看看是否能得到從 Google Analytics 中看到的將同瀏覽數。

下圖中為 AWS Lambda 的介面

  1. 如同 IDE 的檔案夾
  2. 程式碼編輯區塊
  3. 環境變數
  4. 部署按鈕
  5. 測試介面
  6. 測試輸出結果

開發、測試、部署

在測試之前,其實也需要按下 Deploy 的部署按鈕,才能實際拿最新的 Code 來測試。

測試介面

測試完畢後,便能將這個 AWS Lambda Function 公開出去,我們首先進入 Configuration 底下的 Function URL。

設定 Function URL 1

由於呼叫 API 的動作放在 Hexo 產出的前端 Code 之中,我們需要允許所有人取用,因此 Auth type 需設定為 NONE,也就是大家都能呼叫。

設定 Function URL 2

儲存之後,便能夠得到一組 Public URL 實際呼叫了。

最後是一些注意事項,由於 AWS Lambda 有 Cold Start 這個問題,如果 Function 執行的時間過長則會導致呼叫失敗,我嘗試了一些調整後發現目前的程式可以在以下的硬體配置中較為穩定的執行。

調整 Memory 及 Timeout

將 Memory 調整成 512 MB,以及 Timeout 時間為 10 秒,就能穩定執行了。