Color mode

Analytics

Riebeckite では、記事ごとの Page View を収集できます。

Analytics は、次の2つに分かれています。

Package 役割
@riebeckite/plugin-analytics Site 側で Page View を送信する
@riebeckite/analytics-cloudflare Event を受け取り、Cloudflare 上で保存・集計する
Diagram source
text
flowchart LR
    Browser["Browser"]
    Site["Riebeckite Site<br/>Static"]
    Worker["Analytics Worker<br/>Cloudflare"]
    Storage["D1 / KV"]
 
    Browser --> Site
    Browser -->|"page_view"| Worker
    Worker --> Storage

Riebeckite Site 自体はこれまでどおり静的 Site のままです。

Analytics Worker は Site とは別にデプロイします。

Site の wrangler.jsonc や main を Analytics Worker 用に置き換える必要はありません。

どの Package が何をする?

@riebeckite/plugin-analytics

Riebeckite Site 側の Plugin です。

主に、

  • 計測対象 Content の識別
  • Browser からの page_view 送信
  • AnalyticsProvider Contract
  • Analytics Query Contract

を提供します。

Cloudflare、D1、KV、特定の Database には依存しません。

@riebeckite/analytics-cloudflare

Cloudflare 上で Analytics Event を受け取るための独立した Worker Package です。

主に、

  • Event の受信
  • Request Validation
  • CORS
  • Rate Limit
  • D1 / KV への保存
  • Page View の読み取り API

を提供します。

text
Riebeckite Site
  → @riebeckite/plugin-analytics
 
Analytics Worker
  → @riebeckite/analytics-cloudflare

計測の仕組み

Page View は、Content の 安定した Content ID を基準に記録します。

Diagram source
text
sequenceDiagram
    participant C as Content
    participant B as Build
    participant S as Static Site
    participant Browser
    participant W as Analytics Worker
 
    C->>B: stable content ID
    B->>S: data-riebeckite-content-id
    Browser->>S: Page Load
    Browser->>Browser: initAnalytics
    Browser->>W: POST /events

大きく3段階あります。

1. Content ID

計測対象の記事には、Frontmatter で安定した id を指定します。

yaml
---
id: guide-1
---

この ID は記事を識別するための値です。

URL を変更しても同じ Content として扱いたい場合があるため、

text
/old-guide
/new-guide

のような URL を Analytics の Identity として使用しません。

詳しくは Content System を参照してください。

2. Build 時にマーカーを追加する

Build 時に、安定 Content ID を持つ公開 Entry へ隠し要素が追加されます。

html
<span
  hidden
  data-riebeckite-content-id="guide-1"
></span>

Browser 側の Analytics はこのマーカーから Content ID を取得します。

3. Browser から Event を送る

initAnalytics は Document ごとに一度実行されます。

Content ID を取得すると、設定された collectorUrl へ page_view Event を JSON で送信します。

json
{
  "type": "page_view",
  "contentId": "guide-1",
  "occurredAt": "2026-01-01T00:00:00.000Z",
  "path": "/guide",
  "lang": "en"
}

このうち Identity として使われるのは、

text
contentId

です。

path と lang は補助情報です。

安定 Content ID のない Content は計測されません。

Page View の単位

現在の Riebeckite は静的な Document Navigation を使用します。

そのため、

text
Page Load
   ↓
initAnalytics
   ↓
page_view × 1

が基本です。

1回の Page Load につき1件の page_view を送ります。

SPA の Route Transition は自動計測しません。

Site 側を設定する

Site では @riebeckite/plugin-analytics を設定します。

ts
import {
  analytics,
  MemoryAnalyticsProvider,
} from "@riebeckite/plugin-analytics";
 
export default defineConfig({
  // ...
 
  plugins: [
    analytics({
      provider:
        new MemoryAnalyticsProvider(),
 
      publicConfig: {
        collectorUrl:
          "https://analytics.example.com/events",
      },
    }),
  ],
});

主な設定は、

text
provider
publicConfig.collectorUrl

の2つです。

provider

provider は AnalyticsProvider Contract を実装した Runtime です。

Provider は、

text
capabilities
capture
query

を提供します。

認証情報や Storage Binding のような非公開情報は Provider 内部に保持します。

Browser へ渡してはいけません。

publicConfig.collectorUrl

collectorUrl は Browser が Event を送信する URL です。

ts
publicConfig: {
  collectorUrl:
    "https://analytics.example.com/events",
}

指定できるのは、

  • Site-relative Path
  • JSON POST を受け付ける HTTP(S) URL

です。

publicConfig は名前のとおり Browser へ公開されます。

秘密情報を含めないでください。

MemoryAnalyticsProvider

MemoryAnalyticsProvider は、

  • Test
  • Local Experiment

向けです。

本番環境の永続的な Analytics Storage として使用するものではありません。

本番では Cloudflare Worker などの実際の Collector を使用します。

設定を確認する

Analytics Plugin の Option も通常の Plugin Validation の対象です。

たとえば、

  • 不正な provider
  • 不正な collectorUrl

などは、

sh
pnpm exec riebeckite check

や、

sh
pnpm exec riebeckite doctor

で確認できます。

AnalyticsProvider

AnalyticsProvider は、Provider がどの Analytics 機能に対応しているかを capabilities で宣言します。

主な Capability は、

text
capture
content_page_views
popular_content

です。

Event を保存する

ts
await provider.capture(event);

capture は Page View Event を保存します。

Content の Page View を取得する

ts
await provider.query({
  type: "content_page_views",
  contentId: "guide-1",
 
  timeRange: {
    from:
      "2026-01-01T00:00:00.000Z",
  },
});

特定 Content の合計 Page View を取得します。

期間は任意です。

人気 Content を取得する

ts
await provider.query({
  type: "popular_content",
  limit: 10,
});

Page View をもとにしたランキングを取得します。

limit と期間は任意です。

対応していない Query

Provider が対応していない Query には、

text
UnsupportedAnalyticsQueryError

を投げます。

独自 Provider を実装する場合は、

ts
assertAnalyticsQuerySupported(
  provider,
  query,
);

を利用してください。

Capability / Query Helper は Package Root から Export されています。

Cloudflare Worker を使う

Cloudflare で Event を収集する場合は、

text
@riebeckite/analytics-cloudflare

を使用します。

この Package は、

text
createWorker(options)

と Storage Adapter を提供します。

Storage の既定値はありません。

D1 または KV のどちらか1つを明示的に選択します。

D1 と KV

Storage Event 保存 合計 Page View 人気ランキング 期間指定
D1 ○ ○ ○ ○
KV ○ × × ×

D1

D1 では、

text
D1AnalyticsStorage
d1Storage(db)

を使用します。

D1 は UTC の日単位で集計します。

SQLite の Atomic Upsert を利用し、生の Page View Event は保存しません。

そのため、

  • capture
  • content_page_views
  • popular_content
  • 日単位の期間 Query

を利用できます。

本格的に Analytics の集計結果を利用する場合はこちらを使用します。

KV

KV では、

text
KvAnalyticsStorage
kvStorage(namespace)

を使用します。

KV は capture のみ対応します。

Best-effort の集計であり、同時書き込みによって Count が欠落する可能性があります。

そのため、

text
content_page_views
popular_content

には対応しません。

これらの API を利用すると HTTP 501 を返します。

D1 Worker の例

ts
import {
  createWorker,
  d1Storage,
} from "@riebeckite/analytics-cloudflare";
 
export interface Env {
  ANALYTICS_DB: D1Database;
}
 
export default {
  fetch(
    request: Request,
    env: Env,
  ) {
    return createWorker({
      storage:
        d1Storage(env.ANALYTICS_DB),
 
      cors: {
        allowedOrigins: [
          "https://www.example.com",
        ],
      },
    }).fetch(request);
  },
};

Cloudflare の env は fetch の実行時に渡されます。

そのため Storage Binding も Request ごとに構築します。

text
Request
   ↓
fetch(request, env)
   ↓
d1Storage(env.ANALYTICS_DB)
   ↓
createWorker(...)

Module Top-level で env を取得しようとしないでください。

Worker の Endpoint

Analytics Worker は次の Endpoint を提供します。

Endpoint 内容
POST /events page_view を受信
GET /content/:contentId/page-views Content の Page View 合計
GET /popular 人気 Content

POST /events

JSON の page_view Payload を受け付けます。

Body の最大 Size は 8 KiB です。

成功すると、

text
204 No Content

を返します。

不正な Request は拒否されます。

状態 Response
不正な JSON 400
未知・不正な Field 400
JSON 以外の Content-Type 415
8 KiB を超える Body 413
Rate Limit 超過 429

Page View API

text
GET /content/:contentId/page-views?from=&to=

特定 Content の Page View 合計を取得します。

text
GET /popular?limit=&from=&to=

人気 Content を取得します。

読み取り API は Storage Adapter が対応する Capability を宣言している場合だけ利用できます。

CORS

Origin は既定で拒否されます。

そのため、通常は Site の Origin を明示します。

ts
cors: {
  allowedOrigins: [
    "https://www.example.com",
  ],
}

すべての Origin から利用可能にする場合は、

text
"any"

を指定できます。

これは意図的に Public Collector として公開する場合だけ使用してください。

CORS は認証ではない

allowedOrigins を設定しても、Analytics Event が信頼できるようになるわけではありません。

Origin は認証ではないため、第三者が許可された Origin を装って直接、

text
POST /events

を送信することは可能です。

Diagram source
text
flowchart LR
    Site["正規Site"]
    Fake["第三者"]
    Worker["Analytics Worker"]
 
    Site -->|"page_view"| Worker
    Fake -->|"偽造可能"| Worker

そのため、収集した Page View は信頼できない計測データとして扱います。

Rate Limit

濫用を減らすため、任意で rateLimit を設定できます。

Rate Limit は接続元 IP ごとの固定時間窓で Request 数を制限します。

上限を超えると、

text
429 Too Many Requests

を返します。

D1 Rate Limiter

本番環境では、

text
D1AnalyticsRateLimiter
d1RateLimiter(...)

を利用できます。

ts
d1RateLimiter(
  db,
  {
    maxRequests: 100,
    windowMs: 60_000,
  },
);

D1 上で Atomic Counter を管理するため、複数 Worker Isolate 間でも共有できます。

利用する場合は、

text
migrations/0002_analytics_rate_limits.sql

をデプロイ前に適用してください。

Memory Rate Limiter

text
MemoryAnalyticsRateLimiter

は Test / Local Development 用です。

Process 内の Counter しか持たないため、短命な Worker Isolate 間では共有されません。

本番環境の分散した Request を制限する用途には適していません。

Rate Limit の限界

Rate Limit は認証機能ではありません。

できるのは主に、

text
単一Clientからの大量Request
        ↓
一定量まで抑える

ことです。

複数の接続元から分散して Event を送信したり、正規の Page View を偽造したりすることを完全には防げません。

そのため、

CORS + Rate Limit を設定しても Analytics Data 自体を信頼済みデータとして扱わない

ことが重要です。

Privacy

Analytics Worker は、

text
Cookie
User-Agent
Fingerprint

を読み取ったり保存したりしません。

Event に含まれる、

text
path
lang

も補助情報であり、D1 には保存されません。

Rate Limit を有効にした場合だけ、

text
CF-Connecting-IP

を Rate Limit Key として読み取ります。

D1 Rate Limiter では、その Key を有効な Rate Limit Window の間だけ保存します。

D1 を準備する

D1 Schema は Request 中に自動生成しません。

必ずデプロイ前に Migration を適用します。

sh
pnpm exec wrangler d1 execute ANALYTICS_DB \
  --file node_modules/@riebeckite/analytics-cloudflare/migrations/0001_analytics_page_views.sql
 
pnpm exec wrangler d1 execute ANALYTICS_DB \
  --file node_modules/@riebeckite/analytics-cloudflare/migrations/0002_analytics_rate_limits.sql

1つ目は Analytics の集計用 Schema です。

2つ目は D1 Rate Limit を利用する場合に必要です。

Worker をデプロイする

Riebeckite には Analytics Worker 用 Template があります。

text
templates/analytics-cloudflare/d1
templates/analytics-cloudflare/kv

利用する Storage に合わせて、Template を独立した Worker Directory または Repositoryへコピーします。

text
my-site/
  └─ Static Riebeckite Site
 
my-analytics/
  └─ Analytics Worker

Analytics Worker は専用の main を持ちます。

静的 Riebeckite Site の、

text
wrangler.jsonc
main

を置き換えないでください。

Migration を適用したら Worker をデプロイします。

sh
pnpm exec wrangler deploy

デプロイ後、Site の collectorUrl に Worker の /events を設定します。

ts
analytics({
  // ...
 
  publicConfig: {
    collectorUrl:
      "https://analytics.example.com/events",
  },
});

最終的な構成は次のようになります。

Diagram source
text
flowchart LR
    Content["Markdown<br/>stable ID"]
    Build["Riebeckite Build"]
    Site["Static Site"]
    Browser["Browser"]
    Worker["Analytics Worker"]
    D1["D1"]
 
    Content --> Build
    Build --> Site
    Site --> Browser
    Browser -->|"POST /events"| Worker
    Worker --> D1

Diagnostics

@riebeckite/plugin-diagnostics を利用している場合、Analytics Plugin が有効なのに安定 Content ID を持たない公開 Note を検出できます。

この場合、

text
analytics-untracked

という info Diagnostic が報告されます。

たとえば、

text
公開記事
   ↓
stable content ID がない
   ↓
analytics-untracked

となります。

これによって Analytics の計測漏れを、

sh
pnpm exec riebeckite check
pnpm exec riebeckite doctor
pnpm exec riebeckite build

などの Diagnostics から確認できます。

Diagnostics は Content を自動変更しません。

詳しくは Diagnostics を参照してください。

導入の流れ

初めて Analytics を導入する場合は、次の順番で進めると分かりやすくなります。

Diagram source
text
flowchart TD
    ID["1. 計測するContentに<br/>stable IDを付ける"]
    Worker["2. Analytics Workerを作る"]
    Storage["3. D1またはKVを選ぶ"]
    Migration["4. Migrationを適用"]
    Deploy["5. WorkerをDeploy"]
    Plugin["6. analytics Pluginを追加"]
    URL["7. collectorUrlを設定"]
    Check["8. check / doctorで確認"]
 
    ID --> Worker
    Worker --> Storage
    Storage --> Migration
    Migration --> Deploy
    Deploy --> Plugin
    Plugin --> URL
    URL --> Check

本番環境で Page View の集計やランキングを利用する場合は D1 が必要です。

KV は capture のみを必要とする用途に限定してください。

まとめ

Riebeckite Analytics は、静的 Site と Analytics Backend を分離しています。

text
Static Riebeckite Site
  ↓
@riebeckite/plugin-analytics
  ↓
page_view
  ↓
独立した Analytics Worker
  ↓
D1 / KV

Site は静的なまま維持され、Analytics の Storage や Cloudflare 固有処理は Site や Core に入りません。

また、

text
Content Identity
  → stable content ID
 
Public metadata
  → path / lang
 
Storage
  → D1 / KV
 
Abuse mitigation
  → CORS / Rate Limit

という役割を分離しています。

特に、CORS や Rate Limit は Analytics Event の正当性を保証する認証機能ではありません。収集された Page View は信頼できない計測データとして扱ってください。

関連資料

History

1 changesCollapseExpand
1 + # Analytics
2 +
3 + Riebeckite では、記事ごとの Page View を収集できます。
4 +
5 + Analytics は、次の2つに分かれています。
6 +
7 + | Package | 役割 |
8 + | --- | --- |
9 + | `@riebeckite/plugin-analytics` | Site 側で Page View を送信する |
10 + | `@riebeckite/analytics-cloudflare` | Event を受け取り、Cloudflare 上で保存・集計する |
11 +
12 + ```mermaid id="af8k2m"
13 + flowchart LR
14 + Browser["Browser"]
15 + Site["Riebeckite Site<br/>Static"]
16 + Worker["Analytics Worker<br/>Cloudflare"]
17 + Storage["D1 / KV"]
18 +
19 + Browser --> Site
20 + Browser -->|"page_view"| Worker
21 + Worker --> Storage
22 + ```
23 +
24 + **Riebeckite Site 自体はこれまでどおり静的 Site のまま**です。
25 +
26 + Analytics Worker は Site とは別にデプロイします。
27 +
28 + Site の `wrangler.jsonc` や `main` を Analytics Worker 用に置き換える必要はありません。
29 +
30 + ## どの Package が何をする?
31 +
32 + ### `@riebeckite/plugin-analytics`
33 +
34 + Riebeckite Site 側の Plugin です。
35 +
36 + 主に、
37 +
38 + - 計測対象 Content の識別
39 + - Browser からの `page_view` 送信
40 + - `AnalyticsProvider` Contract
41 + - Analytics Query Contract
42 +
43 + を提供します。
44 +
45 + Cloudflare、D1、KV、特定の Database には依存しません。
46 +
47 + ### `@riebeckite/analytics-cloudflare`
48 +
49 + Cloudflare 上で Analytics Event を受け取るための独立した Worker Package です。
50 +
51 + 主に、
52 +
53 + - Event の受信
54 + - Request Validation
55 + - CORS
56 + - Rate Limit
57 + - D1 / KV への保存
58 + - Page View の読み取り API
59 +
60 + を提供します。
61 +
62 + ```text id="w6zh3x"
63 + Riebeckite Site
64 + → @riebeckite/plugin-analytics
65 +
66 + Analytics Worker
67 + → @riebeckite/analytics-cloudflare
68 + ```
69 +
70 + ## 計測の仕組み
71 +
72 + Page View は、Content の **安定した Content ID** を基準に記録します。
73 +
74 + ```mermaid id="n45z2c"
75 + sequenceDiagram
76 + participant C as Content
77 + participant B as Build
78 + participant S as Static Site
79 + participant Browser
80 + participant W as Analytics Worker
81 +
82 + C->>B: stable content ID
83 + B->>S: data-riebeckite-content-id
84 + Browser->>S: Page Load
85 + Browser->>Browser: initAnalytics
86 + Browser->>W: POST /events
87 + ```
88 +
89 + 大きく3段階あります。
90 +
91 + ### 1. Content ID
92 +
93 + 計測対象の記事には、Frontmatter で安定した `id` を指定します。
94 +
95 + ```yaml id="5rmptb"
96 + ---
97 + id: guide-1
98 + ---
99 + ```
100 +
101 + この ID は記事を識別するための値です。
102 +
103 + URL を変更しても同じ Content として扱いたい場合があるため、
104 +
105 + ```text id="2mg98w"
106 + /old-guide
107 + /new-guide
108 + ```
109 +
110 + のような URL を Analytics の Identity として使用しません。
111 +
112 + 詳しくは [Content System](../framework/content-system.ja.md#安定-content-id) を参照してください。
113 +
114 + ### 2. Build 時にマーカーを追加する
115 +
116 + Build 時に、安定 Content ID を持つ公開 Entry へ隠し要素が追加されます。
117 +
118 + ```html id="ly98ut"
119 + <span
120 + hidden
121 + data-riebeckite-content-id="guide-1"
122 + ></span>
123 + ```
124 +
125 + Browser 側の Analytics はこのマーカーから Content ID を取得します。
126 +
127 + ### 3. Browser から Event を送る
128 +
129 + `initAnalytics` は Document ごとに一度実行されます。
130 +
131 + Content ID を取得すると、設定された `collectorUrl` へ `page_view` Event を JSON で送信します。
132 +
133 + ```json id="31i7h9"
134 + {
135 + "type": "page_view",
136 + "contentId": "guide-1",
137 + "occurredAt": "2026-01-01T00:00:00.000Z",
138 + "path": "/guide",
139 + "lang": "en"
140 + }
141 + ```
142 +
143 + このうち Identity として使われるのは、
144 +
145 + ```text id="kkkz5n"
146 + contentId
147 + ```
148 +
149 + です。
150 +
151 + `path` と `lang` は補助情報です。
152 +
153 + 安定 Content ID のない Content は計測されません。
154 +
155 + ## Page View の単位
156 +
157 + 現在の Riebeckite は静的な Document Navigation を使用します。
158 +
159 + そのため、
160 +
161 + ```text id="bs64om"
162 + Page Load
163 + ↓
164 + initAnalytics
165 + ↓
166 + page_view × 1
167 + ```
168 +
169 + が基本です。
170 +
171 + 1回の Page Load につき1件の `page_view` を送ります。
172 +
173 + SPA の Route Transition は自動計測しません。
174 +
175 + ## Site 側を設定する
176 +
177 + Site では `@riebeckite/plugin-analytics` を設定します。
178 +
179 + ```ts id="74prf7"
180 + import {
181 + analytics,
182 + MemoryAnalyticsProvider,
183 + } from "@riebeckite/plugin-analytics";
184 +
185 + export default defineConfig({
186 + // ...
187 +
188 + plugins: [
189 + analytics({
190 + provider:
191 + new MemoryAnalyticsProvider(),
192 +
193 + publicConfig: {
194 + collectorUrl:
195 + "https://analytics.example.com/events",
196 + },
197 + }),
198 + ],
199 + });
200 + ```
201 +
202 + 主な設定は、
203 +
204 + ```text id="3g0p8e"
205 + provider
206 + publicConfig.collectorUrl
207 + ```
208 +
209 + の2つです。
210 +
211 + ### `provider`
212 +
213 + `provider` は `AnalyticsProvider` Contract を実装した Runtime です。
214 +
215 + Provider は、
216 +
217 + ```text id="9ht8ge"
218 + capabilities
219 + capture
220 + query
221 + ```
222 +
223 + を提供します。
224 +
225 + 認証情報や Storage Binding のような非公開情報は Provider 内部に保持します。
226 +
227 + Browser へ渡してはいけません。
228 +
229 + ### `publicConfig.collectorUrl`
230 +
231 + `collectorUrl` は Browser が Event を送信する URL です。
232 +
233 + ```ts id="3xqf6f"
234 + publicConfig: {
235 + collectorUrl:
236 + "https://analytics.example.com/events",
237 + }
238 + ```
239 +
240 + 指定できるのは、
241 +
242 + - Site-relative Path
243 + - JSON `POST` を受け付ける HTTP(S) URL
244 +
245 + です。
246 +
247 + `publicConfig` は名前のとおり Browser へ公開されます。
248 +
249 + 秘密情報を含めないでください。
250 +
251 + ### `MemoryAnalyticsProvider`
252 +
253 + `MemoryAnalyticsProvider` は、
254 +
255 + - Test
256 + - Local Experiment
257 +
258 + 向けです。
259 +
260 + 本番環境の永続的な Analytics Storage として使用するものではありません。
261 +
262 + 本番では Cloudflare Worker などの実際の Collector を使用します。
263 +
264 + ## 設定を確認する
265 +
266 + Analytics Plugin の Option も通常の Plugin Validation の対象です。
267 +
268 + たとえば、
269 +
270 + - 不正な `provider`
271 + - 不正な `collectorUrl`
272 +
273 + などは、
274 +
275 + ```sh id="h5myi0"
276 + pnpm exec riebeckite check
277 + ```
278 +
279 + や、
280 +
281 + ```sh id="kkok81"
282 + pnpm exec riebeckite doctor
283 + ```
284 +
285 + で確認できます。
286 +
287 + ## AnalyticsProvider
288 +
289 + `AnalyticsProvider` は、Provider がどの Analytics 機能に対応しているかを `capabilities` で宣言します。
290 +
291 + 主な Capability は、
292 +
293 + ```text id="31ig1c"
294 + capture
295 + content_page_views
296 + popular_content
297 + ```
298 +
299 + です。
300 +
301 + ### Event を保存する
302 +
303 + ```ts id="6ey01d"
304 + await provider.capture(event);
305 + ```
306 +
307 + `capture` は Page View Event を保存します。
308 +
309 + ### Content の Page View を取得する
310 +
311 + ```ts id="4gohio"
312 + await provider.query({
313 + type: "content_page_views",
314 + contentId: "guide-1",
315 +
316 + timeRange: {
317 + from:
318 + "2026-01-01T00:00:00.000Z",
319 + },
320 + });
321 + ```
322 +
323 + 特定 Content の合計 Page View を取得します。
324 +
325 + 期間は任意です。
326 +
327 + ### 人気 Content を取得する
328 +
329 + ```ts id="3n12mu"
330 + await provider.query({
331 + type: "popular_content",
332 + limit: 10,
333 + });
334 + ```
335 +
336 + Page View をもとにしたランキングを取得します。
337 +
338 + `limit` と期間は任意です。
339 +
340 + ### 対応していない Query
341 +
342 + Provider が対応していない Query には、
343 +
344 + ```text id="mld6b4"
345 + UnsupportedAnalyticsQueryError
346 + ```
347 +
348 + を投げます。
349 +
350 + 独自 Provider を実装する場合は、
351 +
352 + ```ts id="grm9c0"
353 + assertAnalyticsQuerySupported(
354 + provider,
355 + query,
356 + );
357 + ```
358 +
359 + を利用してください。
360 +
361 + Capability / Query Helper は Package Root から Export されています。
362 +
363 + ## Cloudflare Worker を使う
364 +
365 + Cloudflare で Event を収集する場合は、
366 +
367 + ```text id="tfv0z6"
368 + @riebeckite/analytics-cloudflare
369 + ```
370 +
371 + を使用します。
372 +
373 + この Package は、
374 +
375 + ```text id="h5k4u6"
376 + createWorker(options)
377 + ```
378 +
379 + と Storage Adapter を提供します。
380 +
381 + Storage の既定値はありません。
382 +
383 + **D1 または KV のどちらか1つを明示的に選択します。**
384 +
385 + ## D1 と KV
386 +
387 + | Storage | Event 保存 | 合計 Page View | 人気ランキング | 期間指定 |
388 + | --- | --- | --- | --- | --- |
389 + | D1 | ○ | ○ | ○ | ○ |
390 + | KV | ○ | × | × | × |
391 +
392 + ### D1
393 +
394 + D1 では、
395 +
396 + ```text id="a6w8wh"
397 + D1AnalyticsStorage
398 + d1Storage(db)
399 + ```
400 +
401 + を使用します。
402 +
403 + D1 は UTC の日単位で集計します。
404 +
405 + SQLite の Atomic Upsert を利用し、**生の Page View Event は保存しません。**
406 +
407 + そのため、
408 +
409 + - `capture`
410 + - `content_page_views`
411 + - `popular_content`
412 + - 日単位の期間 Query
413 +
414 + を利用できます。
415 +
416 + 本格的に Analytics の集計結果を利用する場合はこちらを使用します。
417 +
418 + ### KV
419 +
420 + KV では、
421 +
422 + ```text id="2dcd0g"
423 + KvAnalyticsStorage
424 + kvStorage(namespace)
425 + ```
426 +
427 + を使用します。
428 +
429 + KV は `capture` のみ対応します。
430 +
431 + Best-effort の集計であり、同時書き込みによって Count が欠落する可能性があります。
432 +
433 + そのため、
434 +
435 + ```text id="qefx4m"
436 + content_page_views
437 + popular_content
438 + ```
439 +
440 + には対応しません。
441 +
442 + これらの API を利用すると HTTP `501` を返します。
443 +
444 + ## D1 Worker の例
445 +
446 + ```ts id="ak02xw"
447 + import {
448 + createWorker,
449 + d1Storage,
450 + } from "@riebeckite/analytics-cloudflare";
451 +
452 + export interface Env {
453 + ANALYTICS_DB: D1Database;
454 + }
455 +
456 + export default {
457 + fetch(
458 + request: Request,
459 + env: Env,
460 + ) {
461 + return createWorker({
462 + storage:
463 + d1Storage(env.ANALYTICS_DB),
464 +
465 + cors: {
466 + allowedOrigins: [
467 + "https://www.example.com",
468 + ],
469 + },
470 + }).fetch(request);
471 + },
472 + };
473 + ```
474 +
475 + Cloudflare の `env` は `fetch` の実行時に渡されます。
476 +
477 + そのため Storage Binding も Request ごとに構築します。
478 +
479 + ```text id="6fak1j"
480 + Request
481 + ↓
482 + fetch(request, env)
483 + ↓
484 + d1Storage(env.ANALYTICS_DB)
485 + ↓
486 + createWorker(...)
487 + ```
488 +
489 + Module Top-level で `env` を取得しようとしないでください。
490 +
491 + ## Worker の Endpoint
492 +
493 + Analytics Worker は次の Endpoint を提供します。
494 +
495 + | Endpoint | 内容 |
496 + | --- | --- |
497 + | `POST /events` | `page_view` を受信 |
498 + | `GET /content/:contentId/page-views` | Content の Page View 合計 |
499 + | `GET /popular` | 人気 Content |
500 +
501 + ### `POST /events`
502 +
503 + JSON の `page_view` Payload を受け付けます。
504 +
505 + Body の最大 Size は 8 KiB です。
506 +
507 + 成功すると、
508 +
509 + ```text id="a3pzla"
510 + 204 No Content
511 + ```
512 +
513 + を返します。
514 +
515 + 不正な Request は拒否されます。
516 +
517 + | 状態 | Response |
518 + | --- | --- |
519 + | 不正な JSON | `400` |
520 + | 未知・不正な Field | `400` |
521 + | JSON 以外の Content-Type | `415` |
522 + | 8 KiB を超える Body | `413` |
523 + | Rate Limit 超過 | `429` |
524 +
525 + ### Page View API
526 +
527 + ```text id="lv9m44"
528 + GET /content/:contentId/page-views?from=&to=
529 + ```
530 +
531 + 特定 Content の Page View 合計を取得します。
532 +
533 + ### Popular API
534 +
535 + ```text id="ckwlsu"
536 + GET /popular?limit=&from=&to=
537 + ```
538 +
539 + 人気 Content を取得します。
540 +
541 + 読み取り API は Storage Adapter が対応する Capability を宣言している場合だけ利用できます。
542 +
543 + ## CORS
544 +
545 + Origin は既定で拒否されます。
546 +
547 + そのため、通常は Site の Origin を明示します。
548 +
549 + ```ts id="esgw6e"
550 + cors: {
551 + allowedOrigins: [
552 + "https://www.example.com",
553 + ],
554 + }
555 + ```
556 +
557 + すべての Origin から利用可能にする場合は、
558 +
559 + ```text id="l9jd7z"
560 + "any"
561 + ```
562 +
563 + を指定できます。
564 +
565 + これは意図的に Public Collector として公開する場合だけ使用してください。
566 +
567 + ## CORS は認証ではない
568 +
569 + `allowedOrigins` を設定しても、Analytics Event が信頼できるようになるわけではありません。
570 +
571 + `Origin` は認証ではないため、第三者が許可された Origin を装って直接、
572 +
573 + ```text id="k2uh06"
574 + POST /events
575 + ```
576 +
577 + を送信することは可能です。
578 +
579 + ```mermaid id="1d65qg"
580 + flowchart LR
581 + Site["正規Site"]
582 + Fake["第三者"]
583 + Worker["Analytics Worker"]
584 +
585 + Site -->|"page_view"| Worker
586 + Fake -->|"偽造可能"| Worker
587 + ```
588 +
589 + そのため、収集した Page View は**信頼できない計測データ**として扱います。
590 +
591 + ## Rate Limit
592 +
593 + 濫用を減らすため、任意で `rateLimit` を設定できます。
594 +
595 + Rate Limit は接続元 IP ごとの固定時間窓で Request 数を制限します。
596 +
597 + 上限を超えると、
598 +
599 + ```text id="sjb59m"
600 + 429 Too Many Requests
601 + ```
602 +
603 + を返します。
604 +
605 + ### D1 Rate Limiter
606 +
607 + 本番環境では、
608 +
609 + ```text id="w2hvbq"
610 + D1AnalyticsRateLimiter
611 + d1RateLimiter(...)
612 + ```
613 +
614 + を利用できます。
615 +
616 + ```ts id="24eqz6"
617 + d1RateLimiter(
618 + db,
619 + {
620 + maxRequests: 100,
621 + windowMs: 60_000,
622 + },
623 + );
624 + ```
625 +
626 + D1 上で Atomic Counter を管理するため、複数 Worker Isolate 間でも共有できます。
627 +
628 + 利用する場合は、
629 +
630 + ```text id="nhoy4m"
631 + migrations/0002_analytics_rate_limits.sql
632 + ```
633 +
634 + をデプロイ前に適用してください。
635 +
636 + ### Memory Rate Limiter
637 +
638 + ```text id="b8ct77"
639 + MemoryAnalyticsRateLimiter
640 + ```
641 +
642 + は Test / Local Development 用です。
643 +
644 + Process 内の Counter しか持たないため、短命な Worker Isolate 間では共有されません。
645 +
646 + 本番環境の分散した Request を制限する用途には適していません。
647 +
648 + ## Rate Limit の限界
649 +
650 + Rate Limit は認証機能ではありません。
651 +
652 + できるのは主に、
653 +
654 + ```text id="ggw3ba"
655 + 単一Clientからの大量Request
656 + ↓
657 + 一定量まで抑える
658 + ```
659 +
660 + ことです。
661 +
662 + 複数の接続元から分散して Event を送信したり、正規の Page View を偽造したりすることを完全には防げません。
663 +
664 + そのため、
665 +
666 + **CORS + Rate Limit を設定しても Analytics Data 自体を信頼済みデータとして扱わない**
667 +
668 + ことが重要です。
669 +
670 + ## Privacy
671 +
672 + Analytics Worker は、
673 +
674 + ```text id="hyx49s"
675 + Cookie
676 + User-Agent
677 + Fingerprint
678 + ```
679 +
680 + を読み取ったり保存したりしません。
681 +
682 + Event に含まれる、
683 +
684 + ```text id="1mxpsk"
685 + path
686 + lang
687 + ```
688 +
689 + も補助情報であり、D1 には保存されません。
690 +
691 + Rate Limit を有効にした場合だけ、
692 +
693 + ```text id="l6fj21"
694 + CF-Connecting-IP
695 + ```
696 +
697 + を Rate Limit Key として読み取ります。
698 +
699 + D1 Rate Limiter では、その Key を有効な Rate Limit Window の間だけ保存します。
700 +
701 + ## D1 を準備する
702 +
703 + D1 Schema は Request 中に自動生成しません。
704 +
705 + 必ずデプロイ前に Migration を適用します。
706 +
707 + ```sh id="ez80bk"
708 + pnpm exec wrangler d1 execute ANALYTICS_DB \
709 + --file node_modules/@riebeckite/analytics-cloudflare/migrations/0001_analytics_page_views.sql
710 +
711 + pnpm exec wrangler d1 execute ANALYTICS_DB \
712 + --file node_modules/@riebeckite/analytics-cloudflare/migrations/0002_analytics_rate_limits.sql
713 + ```
714 +
715 + 1つ目は Analytics の集計用 Schema です。
716 +
717 + 2つ目は D1 Rate Limit を利用する場合に必要です。
718 +
719 + ## Worker をデプロイする
720 +
721 + Riebeckite には Analytics Worker 用 Template があります。
722 +
723 + ```text id="2ynl7a"
724 + templates/analytics-cloudflare/d1
725 + templates/analytics-cloudflare/kv
726 + ```
727 +
728 + 利用する Storage に合わせて、Template を**独立した Worker Directory または Repository**へコピーします。
729 +
730 + ```text id="e2ag5w"
731 + my-site/
732 + └─ Static Riebeckite Site
733 +
734 + my-analytics/
735 + └─ Analytics Worker
736 + ```
737 +
738 + Analytics Worker は専用の `main` を持ちます。
739 +
740 + 静的 Riebeckite Site の、
741 +
742 + ```text id="c24u4u"
743 + wrangler.jsonc
744 + main
745 + ```
746 +
747 + を置き換えないでください。
748 +
749 + Migration を適用したら Worker をデプロイします。
750 +
751 + ```sh id="n1m9do"
752 + pnpm exec wrangler deploy
753 + ```
754 +
755 + デプロイ後、Site の `collectorUrl` に Worker の `/events` を設定します。
756 +
757 + ```ts id="y8ehx2"
758 + analytics({
759 + // ...
760 +
761 + publicConfig: {
762 + collectorUrl:
763 + "https://analytics.example.com/events",
764 + },
765 + });
766 + ```
767 +
768 + 最終的な構成は次のようになります。
769 +
770 + ```mermaid id="l4oz7c"
771 + flowchart LR
772 + Content["Markdown<br/>stable ID"]
773 + Build["Riebeckite Build"]
774 + Site["Static Site"]
775 + Browser["Browser"]
776 + Worker["Analytics Worker"]
777 + D1["D1"]
778 +
779 + Content --> Build
780 + Build --> Site
781 + Site --> Browser
782 + Browser -->|"POST /events"| Worker
783 + Worker --> D1
784 + ```
785 +
786 + ## Diagnostics
787 +
788 + `@riebeckite/plugin-diagnostics` を利用している場合、Analytics Plugin が有効なのに安定 Content ID を持たない公開 Note を検出できます。
789 +
790 + この場合、
791 +
792 + ```text id="s53jzg"
793 + analytics-untracked
794 + ```
795 +
796 + という `info` Diagnostic が報告されます。
797 +
798 + たとえば、
799 +
800 + ```text id="j6n2dx"
801 + 公開記事
802 + ↓
803 + stable content ID がない
804 + ↓
805 + analytics-untracked
806 + ```
807 +
808 + となります。
809 +
810 + これによって Analytics の計測漏れを、
811 +
812 + ```sh id="vl1o09"
813 + pnpm exec riebeckite check
814 + pnpm exec riebeckite doctor
815 + pnpm exec riebeckite build
816 + ```
817 +
818 + などの Diagnostics から確認できます。
819 +
820 + Diagnostics は Content を自動変更しません。
821 +
822 + 詳しくは [Diagnostics](../framework/diagnostics.ja.md) を参照してください。
823 +
824 + ## 導入の流れ
825 +
826 + 初めて Analytics を導入する場合は、次の順番で進めると分かりやすくなります。
827 +
828 + ```mermaid id="zhsgd5"
829 + flowchart TD
830 + ID["1. 計測するContentに<br/>stable IDを付ける"]
831 + Worker["2. Analytics Workerを作る"]
832 + Storage["3. D1またはKVを選ぶ"]
833 + Migration["4. Migrationを適用"]
834 + Deploy["5. WorkerをDeploy"]
835 + Plugin["6. analytics Pluginを追加"]
836 + URL["7. collectorUrlを設定"]
837 + Check["8. check / doctorで確認"]
838 +
839 + ID --> Worker
840 + Worker --> Storage
841 + Storage --> Migration
842 + Migration --> Deploy
843 + Deploy --> Plugin
844 + Plugin --> URL
845 + URL --> Check
846 + ```
847 +
848 + 本番環境で Page View の集計やランキングを利用する場合は D1 が必要です。
849 +
850 + KV は `capture` のみを必要とする用途に限定してください。
851 +
852 + ## まとめ
853 +
854 + Riebeckite Analytics は、静的 Site と Analytics Backend を分離しています。
855 +
856 + ```text id="rnbq84"
857 + Static Riebeckite Site
858 + ↓
859 + @riebeckite/plugin-analytics
860 + ↓
861 + page_view
862 + ↓
863 + 独立した Analytics Worker
864 + ↓
865 + D1 / KV
866 + ```
867 +
868 + Site は静的なまま維持され、Analytics の Storage や Cloudflare 固有処理は Site や Core に入りません。
869 +
870 + また、
871 +
872 + ```text id="d4ph7x"
873 + Content Identity
874 + → stable content ID
875 +
876 + Public metadata
877 + → path / lang
878 +
879 + Storage
880 + → D1 / KV
881 +
882 + Abuse mitigation
883 + → CORS / Rate Limit
884 + ```
885 +
886 + という役割を分離しています。
887 +
888 + 特に、CORS や Rate Limit は Analytics Event の正当性を保証する認証機能ではありません。収集された Page View は信頼できない計測データとして扱ってください。
889 +
890 + ### 関連資料
891 +
892 + - [Content System](../framework/content-system.ja.md#安定-content-id) — 安定 Content ID
893 + - [Diagnostics](../framework/diagnostics.ja.md) — Structured Finding と `check` / `doctor`
894 + - [Reference](../reference/README.ja.md) — 公開 Package と API
895 + - [HonoX Integration](../framework/honox-integration.ja.md) — 静的 Site の Build
896 + - [Deployment](./deployment/README.ja.md) — Site のデプロイ
897 +