Analytics Engine:自己蓋一個分析後台
⚠️ 本章的 SQL API 部分需要 Cloudflare API token,無法在離線環境驗證,已在對應段落標明來源。
LinkForge 現在缺一塊:點擊分析。每一次短網址被點開,我們想記下來源國家、referrer、device、以及回應時間,然後能回答「過去 7 天這個租戶的前 10 大來源國家」。
第 9 章的 D1 是最直覺的答案,但它會在兩個地方撞牆。第一是寫入:D1 是單寫入者架構,一個高流量短網址每秒幾百次點擊,全部變成 INSERT 打向同一個 database,你會先耗盡 rows_written 配額,然後開始看到寫入延遲。第二是查詢:GROUP BY country 掃描一張三個月、上億列的表,這不是 SQLite 該做的事。
Durable Objects(第 14 章)可以做 pre-aggregation —— 每個租戶一顆 DO,在記憶體裡累加計數器。這解決了寫入問題,但你只能查到「你事先決定要累加的那些維度」。事後有人問「前 10 大 referrer 之中,哪些來自 iOS?」你答不出來,因為你當初沒開那個計數器。
Analytics Engine 賣的就是這件事:高基數、無需預先聚合的時序寫入,加上一個 ClickHouse 方言的 SQL 查詢端點。代價是它會抽樣,而抽樣正是本章的核心。
22.1 綁定與寫入
Section titled “22.1 綁定與寫入”{ "analytics_engine_datasets": [ { "binding": "AE", "dataset": "ch22_clicks" }, { "binding": "AE_NO_DATASET" } ]}查 node_modules/wrangler/config-schema.json,這個 binding 的 schema 極其簡單:
{ "type": "object", "properties": { "binding": { "type": "string" }, "dataset": { "type": "string" } }, "required": ["binding"], "additionalProperties": false}只有兩個欄位,dataset 可省略。省略時它預設為 binding 名稱 —— 這一點 wrangler dev 的啟動表格直接印出來給你看:
env.AE (ch22_clicks) Analytics Engine Dataset localenv.AE_NO_DATASET (AE_NO_DATASET) Analytics Engine Dataset local括號裡就是實際的 dataset 名稱。dataset 不需要事先建立,第一次寫入時自動建立。
additionalProperties: false 還告訴我們一件事:這個 binding 沒有 remote 欄位。KV、D1、R2 都支援 "remote": true(第 4 章)把本地開發接到 production 資源,Analytics Engine 不行。實測加上去:
▲ WARNING Processing wrangler.jsonc configuration: - Unexpected fields found in analytics_engine_datasets[0] field: "remote"只是 warning,不是 error —— 也就是說你打錯欄位名,deploy 照樣會成功,而那個欄位靜靜地被忽略。
env.AE.writeDataPoint({ blobs: [slug, country, userAgent, referrer], // 字串維度,最多 20 doubles: [1, latencyMs], // 數值,最多 20 indexes: [slug], // sampling key,只能 1 個});三個陣列都是位置對應的:blobs[0] 進 blob1 欄位,doubles[1] 進 double2,indexes[0] 進 index1。沒有欄位名稱的概念。這代表:
一旦你決定了「
blob2是 country」,就永遠不能改。schema 完全存在於你的腦袋和註解裡,平台不會幫你記。務必在程式碼裡用常數把 schema 寫死。
// 建議:把 schema 集中宣告const CLICK = { blobs: (d: Click) => [d.slug, d.country, d.ua, d.referrer, d.tenantId], doubles: (d: Click) => [1, d.latencyMs], indexes: (d: Click) => [d.tenantId],} as const;// blob1=slug blob2=country blob3=ua blob4=referrer blob5=tenantId// double1=count double2=latencyMs index1=tenantId生成的型別是:
interface AnalyticsEngineDataset { writeDataPoint(event?: AnalyticsEngineDataPoint): void;}interface AnalyticsEngineDataPoint { indexes?: ((ArrayBuffer | string) | null)[]; doubles?: number[]; blobs?: ((ArrayBuffer | string) | null)[];}三件官方文件沒說的事:
(一)參數本身是 optional。 writeDataPoint() 不帶參數在型別上合法。
(二)回傳 void,不是 Promise。 實測 writeDataPoint(...) 回傳 undefined,instanceof Promise 為 false。所以 不需要 await,也不需要 ctx.waitUntil()。文件只說「不用 await」,型別進一步證實它根本不是非同步的 —— runtime 在背景處理實際傳送。
(三)blobs 和 indexes 接受 ArrayBuffer 和 null。 文件只說「字串」。型別明確允許 ArrayBuffer | string | null。null 可以用來佔位(保持位置對應而不寫值),這在 schema 演進時很有用。
22.2 本地 binding 是一個完全不驗證的 no-op
Section titled “22.2 本地 binding 是一個完全不驗證的 no-op”這是本章最重要、也最容易讓人在 production 吃虧的一段。
範例的 /limits 路由把所有文件記載與未記載的限制各戳一次,每一次都包在 try/catch 裡:
out.noArgs = t(() => ae.writeDataPoint()); // 不給參數out.blobs21 = t(() => ae.writeDataPoint({ blobs: Array(21).fill("a") }));out.doubles21 = t(() => ae.writeDataPoint({ doubles: Array(21).fill(1) }));out.indexes2 = t(() => ae.writeDataPoint({ indexes: ["a", "b"] })); // 文件:不會被記錄out.index97 = t(() => ae.writeDataPoint({ indexes: ["x".repeat(97)] }));out.blobs16kPlus1 = t(() => ae.writeDataPoint({ blobs: ["x".repeat(16 * 1024 + 1)] }));out.blobNumber = t(() => ae.writeDataPoint({ blobs: [123 as unknown as string] }));out.doubleNaN = t(() => ae.writeDataPoint({ doubles: [NaN] }));out.doubleInfinity= t(() => ae.writeDataPoint({ doubles: [Infinity] }));out.unknownKey = t(() => ae.writeDataPoint({ blobs: ["u"], tags: ["nope"] } as never));23 個探針,實測結果全部相同:
{ "ok": "undefined", "isPromise": false }沒有任何一個丟出例外,dev server log 裡也沒有任何一行警告。連 /burst?n=300(文件明訂單次 invocation 上限 250 個 data point)也是:
{ "attempted": 300, "firstErrors": [], "errorCount": 0 }再看檔案系統。wrangler dev 會為各種 binding 在 .wrangler/state/v3/ 底下建目錄(cache、workflows、observability…),但沒有 analytics-engine。
把這些拼起來:
protoKeys: ["constructor", "writeDataPoint"]ctorName: "LocalAnalyticsEngineDataset"本地 binding 的類別名字就叫 LocalAnalyticsEngineDataset,它做的事情是:接收參數、什麼都不做、回傳 undefined。不驗證、不儲存、不能查詢。
這代表 Analytics Engine 是本系列到目前為止本地可測性最差的 primitive。 對照一下前面幾章:
| Primitive | 本地能驗證什麼 |
|---|---|
| KV(第 8 章) | 幾乎全部,連 400 You can request a maximum of 100 keys 都會丟 |
| D1(第 9 章) | 全部 SQL 語意,rows_read 也會回報 |
| Queues(第 19 章) | 重試次數、DLQ 路由 |
| Workflows(第 21 章) | 除了 hibernate/replay 之外的全部 |
| Analytics Engine | 什麼都不能 |
實務對策有三層:
第一層 —— 自己驗證。 既然平台不驗,就在寫入的包裝函式裡自己驗,並在開發環境 throw:
const MAX_BLOBS = 20, MAX_DOUBLES = 20, MAX_BLOB_BYTES = 16 * 1024, MAX_INDEX_BYTES = 96;
export function writeClick(ae: AnalyticsEngineDataset, dp: AnalyticsEngineDataPoint, dev: boolean) { if (dev) { const enc = new TextEncoder(); const blobBytes = (dp.blobs ?? []).reduce<number>( (n, b) => n + (typeof b === "string" ? enc.encode(b).length : (b?.byteLength ?? 0)), 0); if ((dp.blobs ?? []).length > MAX_BLOBS) throw new Error("too many blobs"); if ((dp.doubles ?? []).length > MAX_DOUBLES) throw new Error("too many doubles"); if ((dp.indexes ?? []).length > 1) throw new Error("exactly one index"); if (blobBytes > MAX_BLOB_BYTES) throw new Error(`blobs ${blobBytes}B > 16KB`); const idx = dp.indexes?.[0]; if (typeof idx === "string" && enc.encode(idx).length > MAX_INDEX_BYTES) throw new Error("index > 96 bytes"); } ae.writeDataPoint(dp);}第二層 —— 注意 16 KB 是最容易在 production 才炸的那一條。 blobs 總計 16 KB 聽起來很寬鬆,直到你把 user-agent 或 referer 原封不動寫進去。惡意或異常的 referer header 可以輕易是好幾 KB。永遠截斷:
const cut = (s: string | null, n: number) => (s ?? "").slice(0, n);blobs: [cut(slug, 128), country, cut(ua, 512), cut(referrer, 512)]第三層 —— 部署後立刻用 SQL API 對一次帳。 這是唯一能確認「我寫的東西真的進去了、而且欄位位置正確」的方法。詳見 22.5。
一個文件明確記載、但本地測不出來的行為:
indexes給超過 1 個,整個 data point 不會被記錄。不是截斷、不是報錯,是整筆丟掉。這是唯一被官方文件寫出來的「超限後果」;其餘所有限制(>20 blobs、>16 KB、>96 bytes、>250 points)超過之後會怎樣,文件完全沒有說。本章不臆測。
22.3 Sampling:本章的靈魂
Section titled “22.3 Sampling:本章的靈魂”Analytics Engine 不保證儲存你寫進去的每一筆。它在兩個地方抽樣。
寫入端:equitable sampling
Section titled “寫入端:equitable sampling”官方說法是「我們會均衡每個 index 值被儲存的事件數量」。對於罕見的 index 值,可能全部寫入;對於某個 index 值寫入量特別大時,開始抽樣。
這解釋了 indexes 為什麼叫「sampling key」而不是「索引」—— 它的主要作用不是加速查詢,是決定抽樣的分組單位。
實務推論:index 應該選你最常拿來切分資料的那個維度。LinkForge 的例子裡,tenantId 是好的選擇 —— 這樣一個超大租戶的流量被抽樣時,不會連帶把小租戶的資料也抽掉。如果你把 index 設成 country,那麼「台灣」的高流量會壓縮到「台灣」自己的取樣率,而不會影響「冰島」。
官方 FAQ 給了一個量級參考:以他們自家 CDN 的工作負載觀察,每個 index 值大約要每秒 100 筆以上,抽樣才會開始明顯。這是觀察值不是保證。
兩個明確的反面建議:
- 不要用 UUID 之類的唯一值當 index。「這會讓你可以很快撈到單一筆資料,但會拖慢絕大多數聚合與時序查詢。」
- 不要一次查詢跨很多 index 值。「在一次查詢中讀取很多 index,會得到低解析度的資料 —— 可能低到不堪用。」
需要多維度切分時,官方認可的做法是把值串起來("$tenantId:$country"),或者同一份資料用不同 index 寫兩次到兩個 dataset。後者聽起來浪費,但計費是按 data point 計數的定額(22.6),所以成本可預測。
查詢端:ABR(adaptive bit rate)
Section titled “查詢端:ABR(adaptive bit rate)”資料以多種解析度儲存(100%、10%、1%…),查詢時系統依據查詢的複雜度自動挑一個,好讓查詢能在固定時間內完成。查的時間範圍越長,讀到的解析度越低。
_sample_interval:唯一正確的處理方式
Section titled “_sample_interval:唯一正確的處理方式”每一列都帶一個 _sample_interval 欄位,它是該列的取樣率倒數。1% 取樣 → _sample_interval = 100。
關鍵在於官方那句話:
「sample interval 是每一列各自的屬性…由於 equitable sampling 的實作方式,每一列的 sample interval 可能不同。因此查詢時你必須考慮這個欄位。單純把查詢結果乘上一個固定的取樣係數是不夠的。」
所以每一個聚合都要改寫:
| 你想算 | 沒抽樣時會寫 | 正確寫法 |
|---|---|---|
| 事件計數 | count() | sum(_sample_interval) |
| 數值加總 | sum(bytes) | sum(bytes * _sample_interval) |
| 平均 | avg(bytes) | sum(bytes * _sample_interval) / sum(_sample_interval) |
| 分位數 | quantile(0.5)(bytes) | quantileExactWeighted(0.5)(bytes, _sample_interval) |
count() 仍然有用,但意思變了:它是「這次查詢實際讀到幾列」,也就是結果的可信度指標。官方 FAQ 說得很直接:不要用 _sample_interval 判斷準確度,要用 count();「如果你是從一兩列推估出來的,這個結果不太可能有代表性。」
所以正式的儀表板查詢應該同時輸出兩者:
SELECT blob2 AS country, sum(_sample_interval) AS clicks, sum(double2 * _sample_interval) / sum(_sample_interval) AS avg_latency, quantileExactWeighted(0.95)(double2, _sample_interval) AS p95_latency, count() AS rows_readFROM ch22_clicksWHERE timestamp > NOW() - INTERVAL '1' DAYGROUP BY countryORDER BY clicks DESCLIMIT 20FORMAT JSON前端拿到 rows_read 很小的那幾列,就該標上「樣本不足」而不是直接畫成柱狀圖。
抽樣改變了你能問的問題
Section titled “抽樣改變了你能問的問題”這一段是選型的關鍵,值得單獨列出。官方明列的取捨:
- 非 index 欄位的 unique count 不一定準確(例如「有多少個不重複的 referrer」)。
- 非常罕見的非 index 值可能完全看不到。
- 不保證能撈到任何一筆特定紀錄。
- 無法重建精確的事件序列。
所以:Analytics Engine 適合「趨勢、比例、分佈」,不適合「稽核軌跡、逐筆對帳、單筆查詢」。如果 LinkForge 之後要做「這個 API key 在幾點幾分做了什麼」的稽核日誌,那是第 23 章 Pipelines → R2 的工作,不是 Analytics Engine 的。
22.4 資料模型與限制
Section titled “22.4 資料模型與限制”實際的表結構(官方 SQL API 文件):
| 欄位 | 型別 |
|---|---|
dataset | string |
timestamp | DateTime(UTC) |
_sample_interval | integer |
index1 | string |
blob1 … blob20 | string |
double1 … double20 | double |
限制(官方 limits 頁的完整內容,沒有更大的表):
| 限制 | 值 |
|---|---|
| 每個 data point 的 blobs 數 | 20 |
| 每個 data point 的 doubles 數 | 20 |
| 每個 data point 的 indexes 數 | 1 |
| 每個 data point 的 blobs 總大小 | 16 KB |
| 單一 index 大小 | 96 bytes |
| 每次 Worker invocation 的 data point 數 | 250 |
| 資料保留期 | 3 個月 |
文件沒有記載的:寫入速率上限、doubles 的位元組限制、index 基數上限、查詢速率、查詢逾時、結果列數上限。這產品被明確行銷為「unlimited-cardinality」,所以基數上限的缺席可能是刻意的。
保留期 3 個月不可設定。想要更長,官方給的路徑是去 Developers Discord 的 #analytics-engine 頻道說明你的用途。這一點對架構決策影響很大:任何需要跨年比較的報表,都不能只靠 Analytics Engine。要嘛定期把聚合結果寫進 D1,要嘛走第 23 章的 Pipelines → R2 長期歸檔。
22.5 SQL API
Section titled “22.5 SQL API”本節內容來自官方文件;本章的離線驗證環境無法連到
api.cloudflare.com,所以以下未附實測輸出。
端點:
POST https://api.cloudflare.com/client/v4/accounts/<account_id>/analytics_engine/sqlAuthorization: Bearer <token>
<SQL 直接放在 request body>Token 必須是自訂 API token,帶 Account Analytics Read 權限。注意這是 Account 層級權限,不是 Zone。
回應(預設 FORMAT JSON):
{ "meta": [{ "name": "country", "type": "String" }], "data": [{ "country": "TW", "clicks": 1234 }], "rows": 1}FORMAT 支援 JSON(預設)、JSONEachRow、TabSeparated。沒有 CSV —— 官方 statements 參考只列了這三個。
方言的三個硬限制
Section titled “方言的三個硬限制”- 只能查單一表。「
UNION、JOIN等目前不支援。」這一條決定了很多設計:想要跨 dataset 關聯?做不到,只能在應用層合併,或者一開始就把需要的維度寫進同一個 dataset。 - 沒有 DDL / DML。 表由寫入自動建立,不能
ALTER、不能DELETE。schema 演進只能靠「新欄位往後加」或「開新 dataset」。 SHOW TABLES是唯一的目錄查詢。
支援的 SELECT 子句:FROM(單表或子查詢)、WHERE、GROUP BY、HAVING、ORDER BY、LIMIT、OFFSET、FORMAT、AS 別名。
可用函式(節錄):
- 聚合:
count、sum、avg、min、max、quantileExactWeighted、argMin、argMax、topK、topKWeighted、countIf、sumIf、avgIf - 日期時間:
toStartOfInterval、toStartOfHour/Day/FiveMinutes…、formatDateTime、toUnixTimestamp、now、today - 數學:
intDiv、round、floor、ceil、pow、log - 型別轉換:
toUInt8、toUInt32 - 運算子:
LIKE/ILIKE/NOT LIKE/NOT ILIKE(2026-01 才加入)
官方的慣用寫法是拿 intDiv 做整數除法再乘回去:
SELECT intDiv(toUInt32(timestamp), 300) * 300 AS t, sum(_sample_interval) AS clicksFROM ch22_clicksWHERE timestamp > NOW() - INTERVAL '1' DAYGROUP BY tORDER BY tFORMAT JSONt 是 epoch 秒。也可以用 toStartOfFiveMinutes(timestamp) 得到 DateTime,看你前端想吃哪一種。
從 Worker 查詢
Section titled “從 Worker 查詢”沒有「查詢 binding」—— 就是一般的 fetch,token 放 secret:
const res = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/analytics_engine/sql`, { method: "POST", headers: { authorization: `Bearer ${env.CF_API_TOKEN}` }, body: query },);這是一個 subrequest,計入 Worker 的 subrequest 配額,也會算進 Analytics Engine 的「讀取查詢」計費維度。不要在每個使用者請求都打一次;把儀表板查詢的結果快取進 KV(第 8 章)或 Cache API(第 6 章),TTL 幾分鐘。
⚠️ 安全:
env.CF_API_TOKEN是 Account Analytics Read 權限的 token。絕對不要把查詢字串交給前端拼接,那等於開放任意 SQL 給使用者。Worker 端應該只暴露幾個參數化的報表端點,SQL 模板寫死在程式碼裡。多租戶場景下,WHERE blob5 = '<tenantId>'的tenantId必須來自驗證過的 session,不能來自 query string。
Grafana
Section titled “Grafana”官方支援用 Altinity 的 ClickHouse plugin 指向 SQL API URL,加上自訂 Authorization header,並把 DateTime 欄位設成 timestamp 以啟用 $timeSeries / $timeFilter 巨集。這是目前把 Analytics Engine 接到現成儀表板最省事的路。
GraphQL 的疑點
Section titled “GraphQL 的疑點”官方 get-started 頁面說有兩種查詢方式,第二種是 GraphQL API,並連到 Analytics GraphQL 文件。但翻遍 GraphQL 的 datasets 參考,找不到任何一個節點會回傳使用者自己寫入的 Analytics Engine 資料(Workers 相關的只有 workersInvocationsAdaptive,那是平台自己的指標)。
結論:目前實務上就是 SQL API 一條路。get-started 那個連結看起來是過時或誤導。
22.6 計費
Section titled “22.6 計費”官方 pricing 頁列了兩個計費維度,沒有儲存費用:
| 方案 | 寫入 data point | 讀取查詢 |
|---|---|---|
| Workers Paid | 每月含 1,000 萬,超出 $0.25 / 百萬 | 每月含 100 萬,超出 $1.00 / 百萬 |
| Workers Free | 每日 10 萬 | 每日 1 萬 |
兩個定額性質值得注意:
- 每個 data point 花費相同,無論你放了幾個維度、基數多高。所以「多寫幾個 blob」是免費的,「多寫一筆 data point」才要錢。這直接影響設計 —— 寧可一筆 data point 塞滿 20 個 blob,也不要拆成三筆。
- 每個讀取查詢花費相同,無論複雜度或回傳列數。所以「一次查完再前端切分」比「打三次 API」便宜三倍。
一個大到必須加粗的但書,直接引用官方 pricing 頁:
「Currently, you will not be billed for your use of Workers Analytics Engine.」(目前你不會因為使用 Workers Analytics Engine 而被收費。)
費率是預先公告以便你估算成本,尚未實際開始計費。這句話在官方頁面上已經掛了很久。查證日期:2026-08-01。 讀到這篇文章時請自行重新確認 —— 這是全系列少數幾個「今天免費、未來會收費」的 primitive,把它當成免費資源來設計架構是有風險的。
22.7 LinkForge:接上點擊分析
Section titled “22.7 LinkForge:接上點擊分析”放進第 5 章的 Hono 骨架裡:
export type ClickEvent = { tenantId: string; slug: string; country: string; referrer: string; ua: string; latencyMs: number;};
const cut = (s: string | null | undefined, n: number) => (s ?? "").slice(0, n);
// blob1=slug blob2=country blob3=referrer blob4=ua blob5=tenantId// double1=count double2=latencyMs// index1=tenantIdexport function recordClick(ae: AnalyticsEngineDataset, e: ClickEvent): void { ae.writeDataPoint({ blobs: [cut(e.slug, 128), cut(e.country, 2), cut(e.referrer, 512), cut(e.ua, 512), e.tenantId], doubles: [1, e.latencyMs], indexes: [e.tenantId], });}重導向路由:
app.get("/:slug", async (c) => { const started = Date.now(); const target = await resolve(c.env, c.req.param("slug")); if (!target) return c.notFound();
// 同步、非阻塞、不需要 waitUntil recordClick(c.env.AE, { tenantId: target.tenantId, slug: target.slug, country: c.req.raw.cf?.country as string ?? "XX", referrer: c.req.header("referer") ?? "", ua: c.req.header("user-agent") ?? "", latencyMs: Date.now() - started, });
return c.redirect(target.url, 302);});注意 index1 用 tenantId:每個租戶的抽樣率獨立計算,大租戶的流量不會把小租戶的資料抽掉。同時所有租戶層級的報表(絕大多數)都只讀一個 index 值,避免了「跨多 index 查詢解析度低」的問題。
報表端點(記得快取):
app.get("/api/reports/countries", requireAuth, async (c) => { const tenantId = c.get("session").tenantId; // 來自驗證,不是 query string const cacheKey = `report:countries:${tenantId}`; const hit = await c.env.KV.get(cacheKey, "json"); if (hit) return c.json(hit);
const sql = ` SELECT blob2 AS country, sum(_sample_interval) AS clicks, count() AS rows_read FROM linkforge_clicks WHERE index1 = '${tenantId}' AND timestamp > NOW() - INTERVAL '7' DAY GROUP BY country ORDER BY clicks DESC LIMIT 20 FORMAT JSON`;
const data = await queryAE(c.env, sql); await c.env.KV.put(cacheKey, JSON.stringify(data), { expirationTtl: 300 }); return c.json(data);});tenantId 來自 session 而非請求參數,而且我們自己控制它的格式(UUID),所以字串內插在這裡是安全的。如果任何一個值來自使用者輸入,就必須先做嚴格的白名單驗證 —— SQL API 沒有 prepared statement,你沒有第二道防線。
22.8 什麼時候不要用 Analytics Engine
Section titled “22.8 什麼時候不要用 Analytics Engine”官方沒有寫這一段,以下是綜合前面各章實測的判斷。
| 需求 | Analytics Engine | 該用什麼 |
|---|---|---|
| 「上週前 10 大來源國家」 | ✅ 正解 | — |
| 「每 5 分鐘的 p95 延遲趨勢」 | ✅ 正解 | — |
| 「這個 API key 昨天 14:32 做了什麼」 | ❌ 不保證撈得到任何特定一筆 | Pipelines → R2(第 23 章) |
| 「本月精確用量,用來開帳單」 | ⚠️ 抽樣有誤差 | DO 計數器(第 14 章)或 D1 |
| 「即時顯示這篇文章的瀏覽數」 | ❌ 查詢延遲 + 抽樣 | DO(第 14 章) |
| 「跨年度同期比較」 | ❌ 只留 3 個月 | 定期把聚合結果寫進 D1 |
| 「不重複訪客數」 | ❌ 非 index 欄位的 unique count 不準 | 需要另外設計 |
計費相關的用量統計特別值得展開。官方 recipe 頁面有一篇 usage-based billing 的做法,核心技巧是在結帳時「一個客戶跑一次查詢」,因為單一 index 值的查詢受抽樣影響最小。但這仍然是統計估計。真金白銀的計量,我的建議是雙軌:DO 計數器負責精確值(第 14 章實測過它的 input gate 提供的序列化保證),Analytics Engine 負責高維度的分析與異常偵測。
22.9 本章實測結論彙整
Section titled “22.9 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | writeDataPoint 回傳 undefined,非 Promise;型別 (event?) => void | 不需要 await,也不需要 ctx.waitUntil() |
| 2 | 型別允許 blobs/indexes 為 ArrayBuffer | string | null(文件只說字串) | null 可用來佔位以保持欄位位置 |
| 3 | 參數本身 optional,writeDataPoint() 合法且不報錯 | 空寫入是靜默的 |
| 4 | 本地 binding 類別名為 LocalAnalyticsEngineDataset | 明示這是模擬物 |
| 5 | 23 個違規輸入(21 blobs、2 indexes、97B index、16KB+1、NaN、Infinity、錯型別、未知鍵)全部靜默通過 | 平台完全不做本地驗證 |
| 6 | 單次 invocation 寫 300 筆(文件上限 250)本地無錯 | 上限只在 production 生效 |
| 7 | .wrangler/state/v3/ 底下沒有 analytics-engine 目錄 | 本地不持久化、不可查詢 |
| 8 | config schema 只有 binding(必填)與 dataset,additionalProperties: false | 沒有 remote 支援 |
| 9 | 加上 "remote": true 只出 warning,deploy 仍成功 | 欄位打錯會被靜默忽略 |
| 10 | 省略 dataset 時,dev 啟動表格顯示 dataset = binding 名稱 | 文件所述行為得到驗證 |
| 11 | 官方唯一記載的超限後果:indexes > 1 → 整筆不記錄 | 其餘超限行為文件未載,不臆測 |
| 12 | GraphQL API 沒有任何節點可讀自訂 dataset | 實務上只有 SQL API 一條路 |
| 13 | pricing 頁公告費率,同時聲明「目前不會被收費」 | 需標註查證日期,架構上不可假設永久免費 |
22.10 動手練習
Section titled “22.10 動手練習”- 把
/limits的探針改寫成一個assertValidDataPoint()函式,在wrangler dev下 throw、在 production 下只記錄警告,並用 vitest 涵蓋全部 23 個案例(第 38 章會用到這套測試骨架)。 - 為 LinkForge 寫兩個版本的「過去 24 小時每 5 分鐘點擊數」查詢:一個用
count(),一個用sum(_sample_interval)。部署後製造 10 萬次點擊,比較兩者差異。 - 把同一份點擊事件用兩個 index(
tenantId與country)寫進兩個 dataset,比較同一個「前 10 大國家」查詢在兩邊的rows_read。 - 設計一個每天執行的 Cron Trigger(第 20 章),把 Analytics Engine 的日聚合結果寫進 D1,讓 LinkForge 能做超過 3 個月的同期比較。
- Workers Analytics Engine — Get started
- Limits
- Pricing
- Sampling with Workers Analytics Engine
- SQL API
- SQL Reference — Statements
- Querying from a Worker
- Querying from Grafana
- Workers Analytics Engine FAQs
- Usage-based billing recipe
- 設定 schema 來源:
node_modules/wrangler/config-schema.json - 型別來源:
wrangler types產生的worker-configuration.d.ts
下一章(第 23 章):Pipelines 與 R2 SQL —— 當 Analytics Engine 的抽樣與 3 個月保留期不夠用時,把事件原封不動落地成資料湖。注意該章仍在 open beta,且官方文件與 binding 鍵名有已知的不一致。