@riebeckite/plugin-analytics 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -109
- package/README_ja.md +30 -90
- package/dist/client.d.ts +2 -3
- package/dist/client.js +26 -13
- package/dist/index.d.ts +15 -14
- package/dist/index.js +127 -190
- package/dist/src/event.d.ts +23 -0
- package/dist/src/init.d.ts +5 -6
- package/dist/src/memory_provider.d.ts +11 -0
- package/dist/src/options.d.ts +13 -29
- package/dist/src/provider.d.ts +24 -0
- package/dist/src/query.d.ts +26 -0
- package/package.json +5 -4
- package/dist/src/constants.d.ts +0 -18
- package/dist/src/script.d.ts +0 -10
package/README.md
CHANGED
|
@@ -1,136 +1,84 @@
|
|
|
1
1
|
# @riebeckite/plugin-analytics
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Storage-independent analytics primitives and browser page-view tracking for
|
|
4
|
+
Riebeckite. It contains no Cloudflare, Worker, database, or vendor code.
|
|
5
5
|
|
|
6
6
|
[日本語](./README_ja.md)
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Design
|
|
9
9
|
|
|
10
|
-
`
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
- `AnalyticsEvent` includes a typed `page_view` event and can be extended with
|
|
11
|
+
provider-specific event unions.
|
|
12
|
+
- `AnalyticsProvider` declares discoverable capabilities and exposes `capture`
|
|
13
|
+
and `query`. Queries cover per-content page views and popular content, with
|
|
14
|
+
optional ISO-8601 time ranges.
|
|
15
|
+
- `UnsupportedAnalyticsQueryError` makes unsupported query capabilities
|
|
16
|
+
explicit. `MemoryAnalyticsProvider` is included for tests and local examples.
|
|
17
|
+
- Provider runtime configuration (credentials, storage, bindings) stays inside
|
|
18
|
+
the provider. The browser receives only `AnalyticsPublicConfig`.
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
22
|
```ts
|
|
23
|
-
import {
|
|
24
|
-
|
|
23
|
+
import { analytics, MemoryAnalyticsProvider } from "@riebeckite/plugin-analytics";
|
|
24
|
+
|
|
25
|
+
const provider = new MemoryAnalyticsProvider();
|
|
25
26
|
|
|
26
|
-
export default
|
|
27
|
-
// ...
|
|
27
|
+
export default {
|
|
28
28
|
plugins: [
|
|
29
|
-
analytics({
|
|
29
|
+
analytics({
|
|
30
|
+
provider,
|
|
31
|
+
publicConfig: { collectorUrl: "/analytics/events" },
|
|
32
|
+
}),
|
|
30
33
|
],
|
|
31
|
-
}
|
|
34
|
+
};
|
|
32
35
|
```
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
analytics({ provider: "plausible", domain: "example.com" });
|
|
38
|
-
analytics({ provider: "umami", siteId: "xxxxxxxx-xxxx-xxxx" });
|
|
39
|
-
analytics({ provider: "umami", siteId: "…", domain: "example.com" });
|
|
40
|
-
analytics({ provider: "google-analytics", measurementId: "G-XXXXXXXXXX" });
|
|
41
|
-
analytics({ provider: "custom", scriptUrl: "https://cdn.example.com/a.js" });
|
|
42
|
-
analytics({ provider: "custom", snippet: "window.__analytics = true;" });
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Options
|
|
46
|
-
|
|
47
|
-
| Option | Type | Required | Description |
|
|
48
|
-
| ------ | ---- | -------- | ----------- |
|
|
49
|
-
| `provider` | `"plausible" \| "umami" \| "google-analytics" \| "custom"` | yes | Provider whose bootstrap is served |
|
|
50
|
-
| `domain` | `string` | Plausible; optional for Umami | Plausible `data-domain`, or Umami `data-domains` |
|
|
51
|
-
| `siteId` | `string` | Umami | Umami `data-website-id` |
|
|
52
|
-
| `measurementId` | `string` | google-analytics | GA measurement id (`G-…`) |
|
|
53
|
-
| `scriptUrl` | `string` | Provider default | Overrides the provider script URL; the custom script URL when no `snippet` is given |
|
|
54
|
-
| `snippet` | `string` | custom (with no `scriptUrl`) | Raw bootstrap JavaScript, served verbatim |
|
|
55
|
-
| `scriptPath` | `string` | `/_analytics.js` | Endpoint and client script path |
|
|
56
|
-
|
|
57
|
-
`validateOptions` reports missing or mistyped fields through the standard
|
|
58
|
-
Riebeckite config validation, so `riebeckite check` fails fast on a bad setup.
|
|
59
|
-
|
|
60
|
-
Provider script URL defaults:
|
|
37
|
+
`collectorUrl` is intentionally public. It is a relative path or an HTTP(S)
|
|
38
|
+
URL for a collector that accepts a JSON `POST` body. A future provider package
|
|
39
|
+
can expose a collector backed by its own private runtime configuration.
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
| -------- | ----------- |
|
|
64
|
-
| `plausible` | `https://plausible.io/js/script.js` |
|
|
65
|
-
| `umami` | `https://cloud.umami.is/script.js` |
|
|
66
|
-
| `google-analytics` | `https://www.googletagmanager.com/gtag/js` |
|
|
41
|
+
## Browser behavior
|
|
67
42
|
|
|
68
|
-
|
|
43
|
+
For every published content entry with Core's source-authored stable `id` (or
|
|
44
|
+
legacy `uid`), the plugin places a small content-ID marker in rendered HTML and
|
|
45
|
+
registers `initAnalytics` with the generic public-config client mechanism.
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
- `200 OK`
|
|
72
|
-
- `Content-Type: application/javascript; charset=utf-8`
|
|
73
|
-
- `Cache-Control: public, max-age=3600`
|
|
74
|
-
- Body: a self-executing snippet that creates the provider `<script>` tag
|
|
75
|
-
(`async`/`defer` as appropriate). All configured values are JSON-encoded, so
|
|
76
|
-
they cannot break out of their string literals.
|
|
47
|
+
In a browser, the initializer sends exactly one event per document:
|
|
77
48
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
to `document.head`, and is a no-op if that tag already exists.
|
|
88
|
-
|
|
89
|
-
## Privacy and trust boundary
|
|
90
|
-
|
|
91
|
-
- No analytics data is collected at build time; the endpoint only serves code.
|
|
92
|
-
- The `custom` provider's `snippet` (or any `scriptUrl`) is injected as-is into
|
|
93
|
-
visitors' pages. Treat it as trusted configuration, never as user input.
|
|
94
|
-
- Nothing is sanitized or proxied: whatever provider you configure receives the
|
|
95
|
-
requests directly from the browser, under the site's own origin and privacy
|
|
96
|
-
policy.
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "page_view",
|
|
52
|
+
"contentId": "guide-1",
|
|
53
|
+
"occurredAt": "2026-01-01T00:00:00.000Z",
|
|
54
|
+
"path": "/guide",
|
|
55
|
+
"lang": "en"
|
|
56
|
+
}
|
|
57
|
+
```
|
|
97
58
|
|
|
98
|
-
|
|
59
|
+
`path` and `lang` are contextual metadata, not identity. Content without a
|
|
60
|
+
stable ID is not tracked. The initializer is a no-op during builds/SSR and is
|
|
61
|
+
idempotent in a document. Riebeckite's current static document navigation needs
|
|
62
|
+
no SPA route hooks; SPA navigation is not tracked automatically.
|
|
99
63
|
|
|
100
|
-
|
|
101
|
-
built HTML, `onManifestCreated` appends functional tags to every entry:
|
|
64
|
+
## Provider contract
|
|
102
65
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
66
|
+
```ts
|
|
67
|
+
const result = await provider.query({
|
|
68
|
+
type: "popular_content",
|
|
69
|
+
limit: 10,
|
|
70
|
+
timeRange: { from: "2026-01-01T00:00:00.000Z" },
|
|
71
|
+
});
|
|
106
72
|
```
|
|
107
73
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
## Limitations
|
|
113
|
-
|
|
114
|
-
- **Static client path.** The client entry receives no options, so it always
|
|
115
|
-
loads `/_analytics.js`. If you override `scriptPath`, mount the endpoint and
|
|
116
|
-
load it yourself with a custom client entry.
|
|
117
|
-
- **No SPA route tracking.** Client-side routed SPA navigations are not
|
|
118
|
-
reported automatically; configure the provider or send events manually.
|
|
119
|
-
- **Document lifecycle only.** The bootstrap runs once on initial load. Provide
|
|
120
|
-
your own `snippet` if you need a more involved lifecycle.
|
|
121
|
-
- **No consent management.** The plugin does not gate loading on user consent;
|
|
122
|
-
add that in a client entry if your jurisdiction requires it.
|
|
74
|
+
Capabilities are `capture`, `content_page_views`, and `popular_content`. Call
|
|
75
|
+
`assertAnalyticsQuerySupported(provider, query)` when implementing a provider
|
|
76
|
+
that may not support all queries.
|
|
123
77
|
|
|
124
78
|
## Exports
|
|
125
79
|
|
|
126
|
-
- `analytics(
|
|
127
|
-
- `initAnalytics`
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
- `
|
|
131
|
-
- Constants: `ANALYTICS_SCRIPT_PATH`, `ANALYTICS_SCRIPT_ATTRIBUTE`
|
|
132
|
-
- Types: `AnalyticsOptions`, `AnalyticsProvider`
|
|
133
|
-
|
|
134
|
-
## See also
|
|
135
|
-
|
|
136
|
-
- [Plugin guide](../../docs/plugins_en.md)
|
|
80
|
+
- `analytics()` / `analyticsPlugin()`
|
|
81
|
+
- `initAnalytics` (`@riebeckite/plugin-analytics/client`)
|
|
82
|
+
- `MemoryAnalyticsProvider`
|
|
83
|
+
- Event, query/result, provider/capability, and public-config types
|
|
84
|
+
- `UnsupportedAnalyticsQueryError` and capability helpers
|
package/README_ja.md
CHANGED
|
@@ -1,113 +1,53 @@
|
|
|
1
1
|
# @riebeckite/plugin-analytics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Riebeckite 向けの、ストレージや実行環境に依存しないアクセス解析の基盤です。Cloudflare、Worker、データベース、特定ベンダーの実装は含みません。
|
|
4
4
|
|
|
5
5
|
[English](./README.md)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 設計
|
|
8
8
|
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
- `
|
|
13
|
-
|
|
14
|
-
このプラグインはビルド時に完結します。静的ビルド中にプロバイダのコードを実行することはありません。
|
|
9
|
+
- `AnalyticsEvent` は型付きの `page_view` を持ち、プロバイダ固有イベントの union に拡張できます。
|
|
10
|
+
- `AnalyticsProvider` は対応機能を明示し、`capture` と `query` を提供します。コンテンツ別 PV、人気コンテンツ、任意の ISO 8601 期間を扱えます。
|
|
11
|
+
- 未対応クエリは `UnsupportedAnalyticsQueryError` で明示されます。テスト・ローカル用途には `MemoryAnalyticsProvider` を用意しています。
|
|
12
|
+
- 認証情報・ストレージ・ランタイムバインディングはプロバイダ内部の非公開設定です。ブラウザへ渡すのは `AnalyticsPublicConfig` だけです。
|
|
15
13
|
|
|
16
14
|
## 使い方
|
|
17
15
|
|
|
18
16
|
```ts
|
|
19
|
-
import {
|
|
20
|
-
import { analytics } from "@riebeckite/plugin-analytics";
|
|
17
|
+
import { analytics, MemoryAnalyticsProvider } from "@riebeckite/plugin-analytics";
|
|
21
18
|
|
|
22
|
-
export default
|
|
23
|
-
// ...
|
|
19
|
+
export default {
|
|
24
20
|
plugins: [
|
|
25
|
-
analytics({
|
|
21
|
+
analytics({
|
|
22
|
+
provider: new MemoryAnalyticsProvider(),
|
|
23
|
+
publicConfig: { collectorUrl: "/analytics/events" },
|
|
24
|
+
}),
|
|
26
25
|
],
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
プロバイダごとの例:
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
analytics({ provider: "plausible", domain: "example.com" });
|
|
34
|
-
analytics({ provider: "umami", siteId: "xxxxxxxx-xxxx-xxxx" });
|
|
35
|
-
analytics({ provider: "umami", siteId: "…", domain: "example.com" });
|
|
36
|
-
analytics({ provider: "google-analytics", measurementId: "G-XXXXXXXXXX" });
|
|
37
|
-
analytics({ provider: "custom", scriptUrl: "https://cdn.example.com/a.js" });
|
|
38
|
-
analytics({ provider: "custom", snippet: "window.__analytics = true;" });
|
|
26
|
+
};
|
|
39
27
|
```
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| 項目 | 型 | 必須 | 説明 |
|
|
44
|
-
| --- | --- | --- | --- |
|
|
45
|
-
| `provider` | `"plausible" \| "umami" \| "google-analytics" \| "custom"` | はい | 使用するプロバイダ |
|
|
46
|
-
| `domain` | `string` | Plausible では必須、Umami では任意 | Plausible の `data-domain`、または Umami の `data-domains` |
|
|
47
|
-
| `siteId` | `string` | Umami | Umami の `data-website-id` |
|
|
48
|
-
| `measurementId` | `string` | google-analytics | GA の測定 ID(`G-…`) |
|
|
49
|
-
| `scriptUrl` | `string` | プロバイダの既定値 | プロバイダのスクリプト URL を上書きします。`snippet` がない custom では読み込む URL |
|
|
50
|
-
| `snippet` | `string` | custom(`scriptUrl` がない場合) | そのまま配信される生のブートストラップ JavaScript |
|
|
51
|
-
| `scriptPath` | `string` | `/_analytics.js` | エンドポイント兼クライアントスクリプトのパス |
|
|
52
|
-
|
|
53
|
-
`validateOptions` は不足・型違いのフィールドを標準の設定検証として報告するため、設定ミスは `riebeckite check` で早い段階に失敗します。
|
|
54
|
-
|
|
55
|
-
プロバイダの既定スクリプト URL:
|
|
56
|
-
|
|
57
|
-
| プロバイダ | 既定 URL |
|
|
58
|
-
| --- | --- |
|
|
59
|
-
| `plausible` | `https://plausible.io/js/script.js` |
|
|
60
|
-
| `umami` | `https://cloud.umami.is/script.js` |
|
|
61
|
-
| `google-analytics` | `https://www.googletagmanager.com/gtag/js` |
|
|
62
|
-
|
|
63
|
-
## エンドポイントの契約
|
|
29
|
+
`collectorUrl` は公開してよい収集先 URL です。相対パスまたは HTTP(S) URL を指定し、JSON の `POST` を受け付ける収集側を用意します。将来のプロバイダパッケージは、非公開ランタイム設定を使ってその収集側を提供できます。
|
|
64
30
|
|
|
65
|
-
|
|
66
|
-
- `200 OK`
|
|
67
|
-
- `Content-Type: application/javascript; charset=utf-8`
|
|
68
|
-
- `Cache-Control: public, max-age=3600`
|
|
69
|
-
- 本文: プロバイダの `<script>` タグ(必要に応じて `async` / `defer`)を生成する即時実行スニペット。設定値はすべて JSON エンコードされるため、文字列リテラルを壊すことはできません。
|
|
31
|
+
## ブラウザでの動作
|
|
70
32
|
|
|
71
|
-
|
|
33
|
+
Core の安定コンテンツ ID(フロントマターの `id`、互換用の `uid`)がある公開コンテンツにだけ識別子マーカーを出力します。汎用 `publicConfig` 経由で登録された `initAnalytics` は、ブラウザでドキュメントごとに一度だけ次のようなイベントを送ります。
|
|
72
34
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `custom` の `snippet`(および任意の `scriptUrl`)は訪問者のページにそのまま挿入されます。信頼できる設定として扱い、ユーザー入力から組み立てないでください。
|
|
82
|
-
- 内容のサニタイズやプロキシは行いません。設定したプロバイダは、サイト自身のオリジンとプライバシー方針のもとでブラウザから直接リクエストを受け取ります。
|
|
83
|
-
|
|
84
|
-
## 検証しやすさ
|
|
85
|
-
|
|
86
|
-
静的ビルドではクライアント JavaScript を実行しません。そのため、`onManifestCreated` が全エントリの HTML に実際に機能するタグを付与します。
|
|
87
|
-
|
|
88
|
-
```html
|
|
89
|
-
<link rel="preload" as="script" href="/_analytics.js" />
|
|
90
|
-
<script defer src="/_analytics.js" data-riebeckite-analytics></script>
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"type": "page_view",
|
|
38
|
+
"contentId": "guide-1",
|
|
39
|
+
"occurredAt": "2026-01-01T00:00:00.000Z",
|
|
40
|
+
"path": "/guide",
|
|
41
|
+
"lang": "ja"
|
|
42
|
+
}
|
|
91
43
|
```
|
|
92
44
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
## 制限事項
|
|
96
|
-
|
|
97
|
-
- **クライアント側のパスは固定です。** クライアントエントリはオプションを受け取らないため、常に `/_analytics.js` を読み込みます。`scriptPath` を変更する場合は、エンドポイントのマウントと読み込みを独自のクライアントエントリで行ってください。
|
|
98
|
-
- **SPA のルート遷移には対応しません。** クライアントサイドのルーティングによる画面遷移は自動では計測されません。プロバイダ側で設定するか、手動でイベントを送信してください。
|
|
99
|
-
- **ドキュメントのライフサイクルは初回読み込みのみです。** より複雑なライフサイクルが必要な場合は独自の `snippet` を用意してください。
|
|
100
|
-
- **同意管理は行いません。** 法令上必要な場合は、クライアントエントリ側で同意を得るまで読み込みを止めてください。
|
|
45
|
+
`path` と `lang` は補助情報であり、ID ではありません。安定 ID のないコンテンツは誤って計測しません。ビルド時・SSR 時は何もせず、同じドキュメント内での初期化も冪等です。現行リポジトリは静的なドキュメント遷移のため SPA フックは追加しておらず、SPA 遷移は自動計測しません。
|
|
101
46
|
|
|
102
47
|
## 主なエクスポート
|
|
103
48
|
|
|
104
|
-
- `analytics(
|
|
105
|
-
- `initAnalytics
|
|
106
|
-
- `
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
- 型: `AnalyticsOptions`、`AnalyticsProvider`
|
|
110
|
-
|
|
111
|
-
## 関連資料
|
|
112
|
-
|
|
113
|
-
- [プラグインガイド](../../../docs/ja/plugin-system.md)
|
|
49
|
+
- `analytics()` / `analyticsPlugin()`
|
|
50
|
+
- `initAnalytics`(`@riebeckite/plugin-analytics/client`)
|
|
51
|
+
- `MemoryAnalyticsProvider`
|
|
52
|
+
- イベント、クエリ/結果、プロバイダ/機能、公開設定の型
|
|
53
|
+
- `UnsupportedAnalyticsQueryError` と機能確認ヘルパー
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export type { AnalyticsProvider } from "./src/options.js";
|
|
1
|
+
export { ANALYTICS_CONTENT_ID_ATTRIBUTE, initAnalytics, } from "./src/init.js";
|
|
2
|
+
export type { AnalyticsPublicConfig } from "./src/options.js";
|
package/dist/client.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
// src/constants.ts
|
|
2
|
-
var ANALYTICS_SCRIPT_PATH = "/_analytics.js";
|
|
3
|
-
var ANALYTICS_SCRIPT_ATTRIBUTE = "data-riebeckite-analytics";
|
|
4
|
-
|
|
5
1
|
// src/init.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
var ANALYTICS_CONTENT_ID_ATTRIBUTE = "data-riebeckite-content-id";
|
|
3
|
+
var INITIALIZED_ATTRIBUTE = "data-riebeckite-analytics-initialized";
|
|
4
|
+
function initAnalytics(config) {
|
|
5
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
6
|
+
if (document.documentElement.hasAttribute(INITIALIZED_ATTRIBUTE)) return;
|
|
7
|
+
document.documentElement.setAttribute(INITIALIZED_ATTRIBUTE, "");
|
|
8
|
+
const contentId = document.querySelector(`[${ANALYTICS_CONTENT_ID_ATTRIBUTE}]`)?.getAttribute(ANALYTICS_CONTENT_ID_ATTRIBUTE);
|
|
9
|
+
if (!contentId) return;
|
|
10
|
+
const event = {
|
|
11
|
+
type: "page_view",
|
|
12
|
+
contentId,
|
|
13
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14
|
+
path: window.location.pathname,
|
|
15
|
+
...document.documentElement.lang ? { lang: document.documentElement.lang } : {}
|
|
16
|
+
};
|
|
17
|
+
void sendAnalyticsEvent(config.collectorUrl, event);
|
|
18
|
+
}
|
|
19
|
+
async function sendAnalyticsEvent(collectorUrl, event) {
|
|
20
|
+
await fetch(collectorUrl, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify(event),
|
|
24
|
+
keepalive: true,
|
|
25
|
+
credentials: "same-origin"
|
|
26
|
+
}).catch(() => void 0);
|
|
14
27
|
}
|
|
15
28
|
export {
|
|
16
|
-
|
|
29
|
+
ANALYTICS_CONTENT_ID_ATTRIBUTE,
|
|
17
30
|
initAnalytics
|
|
18
31
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { type AnalyticsOptions } from "./src/options.js";
|
|
2
|
-
export {
|
|
3
|
-
export { initAnalytics } from "./src/init.js";
|
|
4
|
-
export
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
2
|
+
export type { AnalyticsCustomEvent, AnalyticsEvent, AnalyticsPageViewEvent, AnalyticsTimeRange, } from "./src/event.js";
|
|
3
|
+
export { ANALYTICS_CONTENT_ID_ATTRIBUTE, initAnalytics } from "./src/init.js";
|
|
4
|
+
export { MemoryAnalyticsProvider } from "./src/memory_provider.js";
|
|
5
|
+
export type { AnalyticsOptions, AnalyticsPublicConfig } from "./src/options.js";
|
|
6
|
+
export { validateAnalyticsOptions } from "./src/options.js";
|
|
7
|
+
export type { AnalyticsCapability, AnalyticsProvider } from "./src/provider.js";
|
|
8
|
+
export { ANALYTICS_CAPABILITIES, assertAnalyticsQuerySupported, requiredCapabilityForQuery, supportsAnalyticsCapability, UnsupportedAnalyticsQueryError, } from "./src/provider.js";
|
|
9
|
+
export type { AnalyticsQuery, AnalyticsResult, ContentPageViewsQuery, ContentPageViewsResult, PopularContentItem, PopularContentQuery, PopularContentResult, } from "./src/query.js";
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* default) and loaded by the `initAnalytics` client entry. Because client
|
|
12
|
-
* entries are static and receive no plugin options, the endpoint path is a
|
|
13
|
-
* compile-time constant; overriding `scriptPath` requires a custom client
|
|
14
|
-
* entry (see the README).
|
|
11
|
+
* Registers browser page-view tracking for content with a stable content ID.
|
|
12
|
+
* Provider runtime state remains server-side; only public collector metadata is
|
|
13
|
+
* registered with the browser entry.
|
|
15
14
|
*/
|
|
16
|
-
export declare function analytics(options: AnalyticsOptions): import("@riebeckite/core").RiebeckitePlugin<
|
|
17
|
-
|
|
15
|
+
export declare function analytics(options: AnalyticsOptions): import("@riebeckite/core").RiebeckitePlugin<Readonly<{
|
|
16
|
+
provider: import("./index.js").AnalyticsProvider;
|
|
17
|
+
publicConfig: import("./client.js").AnalyticsPublicConfig;
|
|
18
|
+
}>>;
|
|
18
19
|
export declare const analyticsPlugin: typeof analytics;
|
package/dist/index.js
CHANGED
|
@@ -1,246 +1,183 @@
|
|
|
1
1
|
// index.ts
|
|
2
2
|
import {
|
|
3
3
|
createClientEntry,
|
|
4
|
-
defineEndpoint,
|
|
5
4
|
definePlugin,
|
|
6
5
|
escapeHtmlAttribute
|
|
7
6
|
} from "@riebeckite/core";
|
|
8
7
|
|
|
9
|
-
// src/
|
|
10
|
-
var
|
|
11
|
-
var
|
|
8
|
+
// src/init.ts
|
|
9
|
+
var ANALYTICS_CONTENT_ID_ATTRIBUTE = "data-riebeckite-content-id";
|
|
10
|
+
var INITIALIZED_ATTRIBUTE = "data-riebeckite-analytics-initialized";
|
|
11
|
+
function initAnalytics(config) {
|
|
12
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
13
|
+
if (document.documentElement.hasAttribute(INITIALIZED_ATTRIBUTE)) return;
|
|
14
|
+
document.documentElement.setAttribute(INITIALIZED_ATTRIBUTE, "");
|
|
15
|
+
const contentId = document.querySelector(`[${ANALYTICS_CONTENT_ID_ATTRIBUTE}]`)?.getAttribute(ANALYTICS_CONTENT_ID_ATTRIBUTE);
|
|
16
|
+
if (!contentId) return;
|
|
17
|
+
const event = {
|
|
18
|
+
type: "page_view",
|
|
19
|
+
contentId,
|
|
20
|
+
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21
|
+
path: window.location.pathname,
|
|
22
|
+
...document.documentElement.lang ? { lang: document.documentElement.lang } : {}
|
|
23
|
+
};
|
|
24
|
+
void sendAnalyticsEvent(config.collectorUrl, event);
|
|
25
|
+
}
|
|
26
|
+
async function sendAnalyticsEvent(collectorUrl, event) {
|
|
27
|
+
await fetch(collectorUrl, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "content-type": "application/json" },
|
|
30
|
+
body: JSON.stringify(event),
|
|
31
|
+
keepalive: true,
|
|
32
|
+
credentials: "same-origin"
|
|
33
|
+
}).catch(() => void 0);
|
|
34
|
+
}
|
|
12
35
|
|
|
13
36
|
// src/options.ts
|
|
14
|
-
var ANALYTICS_PROVIDERS = [
|
|
15
|
-
"plausible",
|
|
16
|
-
"umami",
|
|
17
|
-
"google-analytics",
|
|
18
|
-
"custom"
|
|
19
|
-
];
|
|
20
37
|
function validateAnalyticsOptions(options) {
|
|
21
38
|
const issues = [];
|
|
22
|
-
if (!options) {
|
|
23
|
-
issues.push({ path: "provider", message: providerMessage() });
|
|
24
|
-
return issues;
|
|
25
|
-
}
|
|
26
|
-
if (!isProvider(options.provider)) {
|
|
27
|
-
issues.push({ path: "provider", message: providerMessage() });
|
|
28
|
-
}
|
|
29
|
-
validateOptionalString(options.domain, "domain", issues);
|
|
30
|
-
validateOptionalString(options.siteId, "siteId", issues);
|
|
31
|
-
validateOptionalString(options.measurementId, "measurementId", issues);
|
|
32
|
-
validateOptionalString(options.scriptUrl, "scriptUrl", issues);
|
|
33
|
-
validateOptionalString(options.snippet, "snippet", issues);
|
|
34
|
-
if (options.provider === "plausible" && !hasText(options.domain)) {
|
|
35
|
-
issues.push({ path: "domain", message: "Plausible requires a domain." });
|
|
36
|
-
}
|
|
37
|
-
if (options.provider === "umami" && !hasText(options.siteId)) {
|
|
38
|
-
issues.push({ path: "siteId", message: "Umami requires a site id." });
|
|
39
|
-
}
|
|
40
|
-
if (options.provider === "google-analytics" && !hasText(options.measurementId)) {
|
|
39
|
+
if (!options || !isAnalyticsProvider(options.provider)) {
|
|
41
40
|
issues.push({
|
|
42
|
-
path: "
|
|
43
|
-
message: "
|
|
41
|
+
path: "provider",
|
|
42
|
+
message: "Expected an analytics provider."
|
|
44
43
|
});
|
|
45
44
|
}
|
|
46
|
-
if (
|
|
45
|
+
if (!options?.publicConfig || !isCollectorUrl(options.publicConfig.collectorUrl)) {
|
|
47
46
|
issues.push({
|
|
48
|
-
path: "
|
|
49
|
-
message: "
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (options.scriptPath !== void 0 && !isStaticPath(options.scriptPath)) {
|
|
53
|
-
issues.push({
|
|
54
|
-
path: "scriptPath",
|
|
55
|
-
message: 'Expected a static path starting with "/".'
|
|
47
|
+
path: "publicConfig.collectorUrl",
|
|
48
|
+
message: "Expected a non-empty absolute URL or site-relative path."
|
|
56
49
|
});
|
|
57
50
|
}
|
|
58
51
|
return issues;
|
|
59
52
|
}
|
|
60
|
-
function
|
|
61
|
-
return typeof value === "
|
|
62
|
-
}
|
|
63
|
-
function providerMessage() {
|
|
64
|
-
return `Expected one of: ${ANALYTICS_PROVIDERS.join(", ")}.`;
|
|
53
|
+
function isAnalyticsProvider(value) {
|
|
54
|
+
return typeof value === "object" && value !== null && "capabilities" in value && "capture" in value && typeof value.capture === "function" && "query" in value && typeof value.query === "function";
|
|
65
55
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (value
|
|
71
|
-
|
|
56
|
+
function isCollectorUrl(value) {
|
|
57
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (value.startsWith("/")) return !value.startsWith("//");
|
|
61
|
+
try {
|
|
62
|
+
const url = new URL(value);
|
|
63
|
+
return url.protocol === "https:" || url.protocol === "http:";
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
72
66
|
}
|
|
73
|
-
}
|
|
74
|
-
function isStaticPath(value) {
|
|
75
|
-
return value.startsWith("/") && value.length > 1 && !/[\s?#:]/.test(value) && !value.includes("//") && value !== "/assets" && !value.startsWith("/assets/");
|
|
76
67
|
}
|
|
77
68
|
|
|
78
|
-
// src/
|
|
79
|
-
var
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
default:
|
|
93
|
-
return "/* @riebeckite/plugin-analytics: unsupported provider */\n";
|
|
69
|
+
// src/provider.ts
|
|
70
|
+
var ANALYTICS_CAPABILITIES = [
|
|
71
|
+
"capture",
|
|
72
|
+
"content_page_views",
|
|
73
|
+
"popular_content"
|
|
74
|
+
];
|
|
75
|
+
function supportsAnalyticsCapability(provider, capability) {
|
|
76
|
+
return provider.capabilities.has(capability);
|
|
77
|
+
}
|
|
78
|
+
var UnsupportedAnalyticsQueryError = class extends Error {
|
|
79
|
+
constructor(query, requiredCapability) {
|
|
80
|
+
super(`Analytics provider does not support ${requiredCapability} queries.`);
|
|
81
|
+
this.query = query;
|
|
82
|
+
this.requiredCapability = requiredCapability;
|
|
94
83
|
}
|
|
84
|
+
query;
|
|
85
|
+
requiredCapability;
|
|
86
|
+
name = "UnsupportedAnalyticsQueryError";
|
|
87
|
+
};
|
|
88
|
+
function requiredCapabilityForQuery(query) {
|
|
89
|
+
return query.type;
|
|
95
90
|
}
|
|
96
|
-
function
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"/* @riebeckite/plugin-analytics: plausible */",
|
|
101
|
-
"(function () {",
|
|
102
|
-
' var script = document.createElement("script");',
|
|
103
|
-
" script.defer = true;",
|
|
104
|
-
` script.setAttribute("data-domain", ${jsString(domain)});`,
|
|
105
|
-
` script.src = ${jsString(url)};`,
|
|
106
|
-
" document.head.appendChild(script);",
|
|
107
|
-
"})();",
|
|
108
|
-
""
|
|
109
|
-
].join("\n");
|
|
110
|
-
}
|
|
111
|
-
function buildUmamiScript(options) {
|
|
112
|
-
const siteId = options.siteId ?? "";
|
|
113
|
-
const domain = options.domain;
|
|
114
|
-
const url = options.scriptUrl ?? UMAMI_SCRIPT_URL;
|
|
115
|
-
const lines = [
|
|
116
|
-
"/* @riebeckite/plugin-analytics: umami */",
|
|
117
|
-
"(function () {",
|
|
118
|
-
' var script = document.createElement("script");',
|
|
119
|
-
" script.defer = true;",
|
|
120
|
-
` script.setAttribute("data-website-id", ${jsString(siteId)});`
|
|
121
|
-
];
|
|
122
|
-
if (domain) {
|
|
123
|
-
lines.push(` script.setAttribute("data-domains", ${jsString(domain)});`);
|
|
91
|
+
function assertAnalyticsQuerySupported(provider, query) {
|
|
92
|
+
const capability = requiredCapabilityForQuery(query);
|
|
93
|
+
if (!supportsAnalyticsCapability(provider, capability)) {
|
|
94
|
+
throw new UnsupportedAnalyticsQueryError(query, capability);
|
|
124
95
|
}
|
|
125
|
-
lines.push(
|
|
126
|
-
` script.src = ${jsString(url)};`,
|
|
127
|
-
" document.head.appendChild(script);",
|
|
128
|
-
"})();",
|
|
129
|
-
""
|
|
130
|
-
);
|
|
131
|
-
return lines.join("\n");
|
|
132
|
-
}
|
|
133
|
-
function buildGoogleAnalyticsScript(options) {
|
|
134
|
-
const measurementId = options.measurementId ?? "";
|
|
135
|
-
const base = options.scriptUrl ?? GOOGLE_ANALYTICS_SCRIPT_URL;
|
|
136
|
-
const source = `${base}?id=${encodeURIComponent(measurementId)}`;
|
|
137
|
-
return [
|
|
138
|
-
"/* @riebeckite/plugin-analytics: google-analytics */",
|
|
139
|
-
"(function () {",
|
|
140
|
-
` var id = ${jsString(measurementId)};`,
|
|
141
|
-
' var script = document.createElement("script");',
|
|
142
|
-
" script.async = true;",
|
|
143
|
-
` script.src = ${jsString(source)};`,
|
|
144
|
-
" document.head.appendChild(script);",
|
|
145
|
-
" window.dataLayer = window.dataLayer || [];",
|
|
146
|
-
" function gtag() { window.dataLayer.push(arguments); }",
|
|
147
|
-
" window.gtag = gtag;",
|
|
148
|
-
' gtag("js", new Date());',
|
|
149
|
-
' gtag("config", id);',
|
|
150
|
-
"})();",
|
|
151
|
-
""
|
|
152
|
-
].join("\n");
|
|
153
96
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
97
|
+
|
|
98
|
+
// src/memory_provider.ts
|
|
99
|
+
var MemoryAnalyticsProvider = class {
|
|
100
|
+
capabilities = new Set(ANALYTICS_CAPABILITIES);
|
|
101
|
+
#events;
|
|
102
|
+
constructor(initialEvents = []) {
|
|
103
|
+
this.#events = [...initialEvents];
|
|
158
104
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
105
|
+
async capture(event) {
|
|
106
|
+
this.#events.push(event);
|
|
107
|
+
}
|
|
108
|
+
async query(query) {
|
|
109
|
+
assertAnalyticsQuerySupported(this, query);
|
|
110
|
+
const pageViews = this.#events.filter(isPageView).filter((event) => isInTimeRange(event.occurredAt, query.timeRange));
|
|
111
|
+
if (query.type === "content_page_views") {
|
|
112
|
+
return {
|
|
113
|
+
type: query.type,
|
|
114
|
+
contentId: query.contentId,
|
|
115
|
+
pageViews: pageViews.filter(
|
|
116
|
+
(event) => event.contentId === query.contentId
|
|
117
|
+
).length
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const counts = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const event of pageViews) {
|
|
122
|
+
counts.set(event.contentId, (counts.get(event.contentId) ?? 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
const items = [...counts.entries()].map(([contentId, pageViews2]) => ({ contentId, pageViews: pageViews2 })).sort(
|
|
125
|
+
(left, right) => right.pageViews - left.pageViews || left.contentId.localeCompare(right.contentId)
|
|
126
|
+
).slice(0, query.limit ?? 10);
|
|
127
|
+
return { type: query.type, items };
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function isPageView(event) {
|
|
131
|
+
return event.type === "page_view";
|
|
174
132
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (typeof document === "undefined") return;
|
|
179
|
-
if (document.querySelector(`script[${ANALYTICS_SCRIPT_ATTRIBUTE}]`)) return;
|
|
180
|
-
const script = document.createElement("script");
|
|
181
|
-
script.defer = true;
|
|
182
|
-
script.src = ANALYTICS_SCRIPT_PATH;
|
|
183
|
-
script.setAttribute(ANALYTICS_SCRIPT_ATTRIBUTE, "");
|
|
184
|
-
document.head.appendChild(script);
|
|
133
|
+
function isInTimeRange(occurredAt, timeRange) {
|
|
134
|
+
if (!timeRange) return true;
|
|
135
|
+
return (timeRange.from === void 0 || occurredAt >= timeRange.from) && (timeRange.to === void 0 || occurredAt <= timeRange.to);
|
|
185
136
|
}
|
|
186
137
|
|
|
187
138
|
// index.ts
|
|
188
139
|
function analytics(options) {
|
|
189
|
-
const scriptPath = options.scriptPath ?? ANALYTICS_SCRIPT_PATH;
|
|
190
140
|
const tracked = /* @__PURE__ */ new Map();
|
|
191
141
|
return definePlugin({
|
|
192
142
|
name: "analytics",
|
|
193
143
|
options,
|
|
194
144
|
validateOptions: validateAnalyticsOptions,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
scriptPath,
|
|
198
|
-
() => ({
|
|
199
|
-
headers: {
|
|
200
|
-
"content-type": "application/javascript; charset=utf-8"
|
|
201
|
-
},
|
|
202
|
-
body: buildAnalyticsScript(options)
|
|
203
|
-
}),
|
|
204
|
-
{ cacheControl: "public, max-age=3600" }
|
|
205
|
-
)
|
|
145
|
+
clientEntries: [
|
|
146
|
+
createClientEntry("analytics", "initAnalytics", options.publicConfig)
|
|
206
147
|
],
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
tracked.set(context.slug, context.content);
|
|
148
|
+
onPostProcessed: ({ slug, content }) => {
|
|
149
|
+
tracked.set(slug, content);
|
|
210
150
|
},
|
|
211
|
-
onManifestCreated: (
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
appendAnalyticsTags(entry, tracked.get(entry.slug), tags);
|
|
151
|
+
onManifestCreated: ({ manifest }) => {
|
|
152
|
+
for (const entry of manifest.entries) {
|
|
153
|
+
appendContentIdentity(entry, tracked.get(entry.slug));
|
|
215
154
|
}
|
|
216
155
|
}
|
|
217
156
|
});
|
|
218
157
|
}
|
|
219
158
|
var analyticsPlugin = analytics;
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
`<script defer src="${href}" ${ANALYTICS_SCRIPT_ATTRIBUTE}></script>`
|
|
225
|
-
].join("\n");
|
|
226
|
-
}
|
|
227
|
-
function appendAnalyticsTags(entry, content, tags) {
|
|
228
|
-
if (!entry.html.includes(ANALYTICS_SCRIPT_ATTRIBUTE)) {
|
|
159
|
+
function appendContentIdentity(entry, content) {
|
|
160
|
+
if (!entry.contentId) return;
|
|
161
|
+
const marker = `<span hidden ${ANALYTICS_CONTENT_ID_ATTRIBUTE}="${escapeHtmlAttribute(entry.contentId)}"></span>`;
|
|
162
|
+
if (!entry.html.includes(ANALYTICS_CONTENT_ID_ATTRIBUTE)) {
|
|
229
163
|
entry.html = `${entry.html}
|
|
230
|
-
${
|
|
164
|
+
${marker}`;
|
|
231
165
|
}
|
|
232
|
-
if (content && !content.html.includes(
|
|
166
|
+
if (content && !content.html.includes(ANALYTICS_CONTENT_ID_ATTRIBUTE)) {
|
|
233
167
|
content.html = `${content.html}
|
|
234
|
-
${
|
|
168
|
+
${marker}`;
|
|
235
169
|
}
|
|
236
170
|
}
|
|
237
171
|
export {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
172
|
+
ANALYTICS_CAPABILITIES,
|
|
173
|
+
ANALYTICS_CONTENT_ID_ATTRIBUTE,
|
|
174
|
+
MemoryAnalyticsProvider,
|
|
175
|
+
UnsupportedAnalyticsQueryError,
|
|
241
176
|
analytics,
|
|
242
177
|
analyticsPlugin,
|
|
243
|
-
|
|
178
|
+
assertAnalyticsQuerySupported,
|
|
244
179
|
initAnalytics,
|
|
180
|
+
requiredCapabilityForQuery,
|
|
181
|
+
supportsAnalyticsCapability,
|
|
245
182
|
validateAnalyticsOptions
|
|
246
183
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** An ISO 8601 time interval. Bounds are inclusive when a provider supports them. */
|
|
2
|
+
export type AnalyticsTimeRange = Readonly<{
|
|
3
|
+
from?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
}>;
|
|
6
|
+
/** A content page view always uses the stable, source-authored content ID. */
|
|
7
|
+
export type AnalyticsPageViewEvent = Readonly<{
|
|
8
|
+
type: "page_view";
|
|
9
|
+
contentId: string;
|
|
10
|
+
occurredAt: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
lang?: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** Base shape for provider-specific events. Consumers can narrow this type. */
|
|
15
|
+
export type AnalyticsCustomEvent = Readonly<{
|
|
16
|
+
type: string;
|
|
17
|
+
occurredAt: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Events accepted by an analytics provider. Supplying a custom event union
|
|
21
|
+
* preserves the strongly typed built-in `page_view` event.
|
|
22
|
+
*/
|
|
23
|
+
export type AnalyticsEvent<TCustomEvent extends AnalyticsCustomEvent = AnalyticsCustomEvent> = AnalyticsPageViewEvent | TCustomEvent;
|
package/dist/src/init.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import type { AnalyticsPublicConfig } from "./options.js";
|
|
2
|
+
export declare const ANALYTICS_CONTENT_ID_ATTRIBUTE = "data-riebeckite-content-id";
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* This initializer is static: it always loads `ANALYTICS_SCRIPT_PATH`. It has
|
|
6
|
-
* no SPA route awareness — see the README for the documented limitations.
|
|
4
|
+
* Sends one document-level page view. Static Riebeckite sites reload documents
|
|
5
|
+
* on navigation; SPA history hooks are intentionally not installed.
|
|
7
6
|
*/
|
|
8
|
-
export declare function initAnalytics(): void;
|
|
7
|
+
export declare function initAnalytics(config: AnalyticsPublicConfig): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnalyticsEvent } from "./event.js";
|
|
2
|
+
import { type AnalyticsProvider } from "./provider.js";
|
|
3
|
+
import type { AnalyticsQuery, AnalyticsResult } from "./query.js";
|
|
4
|
+
/** In-memory provider for tests, local experiments, and provider contract examples. */
|
|
5
|
+
export declare class MemoryAnalyticsProvider implements AnalyticsProvider {
|
|
6
|
+
#private;
|
|
7
|
+
readonly capabilities: Set<"content_page_views" | "popular_content" | "capture">;
|
|
8
|
+
constructor(initialEvents?: readonly AnalyticsEvent[]);
|
|
9
|
+
capture(event: AnalyticsEvent): Promise<void>;
|
|
10
|
+
query(query: AnalyticsQuery): Promise<AnalyticsResult>;
|
|
11
|
+
}
|
package/dist/src/options.d.ts
CHANGED
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
import type { ConfigValidationIssue } from "@riebeckite/core";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import type { AnalyticsProvider } from "./provider.js";
|
|
3
|
+
/** Browser-safe configuration. It must never contain provider credentials. */
|
|
4
|
+
export type AnalyticsPublicConfig = Readonly<{
|
|
5
|
+
/** Public collector URL. The browser POSTs JSON analytics events here. */
|
|
6
|
+
collectorUrl: string;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* Private provider runtime and public browser configuration are deliberately
|
|
10
|
+
* separate. A provider can retain credentials, bindings, and storage handles.
|
|
11
|
+
*/
|
|
12
|
+
export type AnalyticsOptions = Readonly<{
|
|
7
13
|
provider: AnalyticsProvider;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/** Umami website id (`data-website-id`). */
|
|
11
|
-
siteId?: string;
|
|
12
|
-
/** Google Analytics measurement id (`G-XXXXXXX`). */
|
|
13
|
-
measurementId?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Overrides the provider's default script URL. For `provider: "custom"` this
|
|
16
|
-
* is the script URL loaded when no `snippet` is given.
|
|
17
|
-
*/
|
|
18
|
-
scriptUrl?: string;
|
|
19
|
-
/**
|
|
20
|
-
* Raw bootstrap JavaScript for `provider: "custom"`. This value is trusted:
|
|
21
|
-
* it is served verbatim from the endpoint. Do not build it from untrusted
|
|
22
|
-
* input.
|
|
23
|
-
*/
|
|
24
|
-
snippet?: string;
|
|
25
|
-
/**
|
|
26
|
-
* Endpoint and client script path. Defaults to `ANALYTICS_SCRIPT_PATH`
|
|
27
|
-
* (`/_analytics.js`). The built-in client entry always uses the default, so
|
|
28
|
-
* changing this requires a matching custom client entry.
|
|
29
|
-
*/
|
|
30
|
-
scriptPath?: string;
|
|
31
|
-
};
|
|
14
|
+
publicConfig: AnalyticsPublicConfig;
|
|
15
|
+
}>;
|
|
32
16
|
export declare function validateAnalyticsOptions(options: AnalyticsOptions | undefined): readonly ConfigValidationIssue[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AnalyticsEvent } from "./event.js";
|
|
2
|
+
import type { AnalyticsQuery, AnalyticsResult } from "./query.js";
|
|
3
|
+
export declare const ANALYTICS_CAPABILITIES: readonly ["capture", "content_page_views", "popular_content"];
|
|
4
|
+
export type AnalyticsCapability = (typeof ANALYTICS_CAPABILITIES)[number];
|
|
5
|
+
/**
|
|
6
|
+
* Storage- and runtime-independent analytics boundary.
|
|
7
|
+
*
|
|
8
|
+
* Implementations may keep private credentials or runtime bindings internally.
|
|
9
|
+
* Only an explicit `AnalyticsPublicConfig` is ever passed to browser code.
|
|
10
|
+
*/
|
|
11
|
+
export interface AnalyticsProvider {
|
|
12
|
+
readonly capabilities: ReadonlySet<AnalyticsCapability>;
|
|
13
|
+
capture(event: AnalyticsEvent): Promise<void>;
|
|
14
|
+
query(query: AnalyticsQuery): Promise<AnalyticsResult>;
|
|
15
|
+
}
|
|
16
|
+
export declare function supportsAnalyticsCapability(provider: AnalyticsProvider, capability: AnalyticsCapability): boolean;
|
|
17
|
+
export declare class UnsupportedAnalyticsQueryError extends Error {
|
|
18
|
+
readonly query: AnalyticsQuery;
|
|
19
|
+
readonly requiredCapability: AnalyticsCapability;
|
|
20
|
+
readonly name = "UnsupportedAnalyticsQueryError";
|
|
21
|
+
constructor(query: AnalyticsQuery, requiredCapability: AnalyticsCapability);
|
|
22
|
+
}
|
|
23
|
+
export declare function requiredCapabilityForQuery(query: AnalyticsQuery): AnalyticsCapability;
|
|
24
|
+
export declare function assertAnalyticsQuerySupported(provider: AnalyticsProvider, query: AnalyticsQuery): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnalyticsTimeRange } from "./event.js";
|
|
2
|
+
export type ContentPageViewsQuery = Readonly<{
|
|
3
|
+
type: "content_page_views";
|
|
4
|
+
contentId: string;
|
|
5
|
+
timeRange?: AnalyticsTimeRange;
|
|
6
|
+
}>;
|
|
7
|
+
export type PopularContentQuery = Readonly<{
|
|
8
|
+
type: "popular_content";
|
|
9
|
+
limit?: number;
|
|
10
|
+
timeRange?: AnalyticsTimeRange;
|
|
11
|
+
}>;
|
|
12
|
+
export type AnalyticsQuery = ContentPageViewsQuery | PopularContentQuery;
|
|
13
|
+
export type ContentPageViewsResult = Readonly<{
|
|
14
|
+
type: "content_page_views";
|
|
15
|
+
contentId: string;
|
|
16
|
+
pageViews: number;
|
|
17
|
+
}>;
|
|
18
|
+
export type PopularContentItem = Readonly<{
|
|
19
|
+
contentId: string;
|
|
20
|
+
pageViews: number;
|
|
21
|
+
}>;
|
|
22
|
+
export type PopularContentResult = Readonly<{
|
|
23
|
+
type: "popular_content";
|
|
24
|
+
items: readonly PopularContentItem[];
|
|
25
|
+
}>;
|
|
26
|
+
export type AnalyticsResult = ContentPageViewsResult | PopularContentResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-analytics",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@riebeckite/core": "0.0.
|
|
23
|
+
"@riebeckite/core": "0.0.3"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bugs": {
|
|
41
41
|
"url": "https://github.com/Rerurate514/riebeckite/issues"
|
|
42
42
|
},
|
|
43
|
-
"description": "
|
|
43
|
+
"description": "Storage-independent analytics foundation for Riebeckite sites.",
|
|
44
44
|
"keywords": [
|
|
45
45
|
"riebeckite",
|
|
46
46
|
"plugin",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"sideEffects": false,
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build": "node ../../../scripts/build_package.mjs"
|
|
55
|
+
"build": "node ../../../scripts/build_package.mjs",
|
|
56
|
+
"test": "pnpm exec tsx --test \"test/*.test.ts\""
|
|
56
57
|
}
|
|
57
58
|
}
|
package/dist/src/constants.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Default path of the analytics bootstrap endpoint and of the `<script>` tag
|
|
3
|
-
* the client entry injects.
|
|
4
|
-
*
|
|
5
|
-
* Client entries are static — they receive no plugin options — so the built-in
|
|
6
|
-
* `initAnalytics` always loads this exact path. Overriding `scriptPath` mounts
|
|
7
|
-
* the endpoint elsewhere and requires a custom client entry; the default is
|
|
8
|
-
* kept here so both sides share one compile-time constant.
|
|
9
|
-
*/
|
|
10
|
-
export declare const ANALYTICS_SCRIPT_PATH = "/_analytics.js";
|
|
11
|
-
/**
|
|
12
|
-
* `data-*` attribute that marks the analytics `<script>` tag.
|
|
13
|
-
*
|
|
14
|
-
* The server-side injection (`onManifestCreated`) and the browser initializer
|
|
15
|
-
* (`initAnalytics`) both carry this attribute, so a built page can be verified
|
|
16
|
-
* from its real output and the initializer stays idempotent.
|
|
17
|
-
*/
|
|
18
|
-
export declare const ANALYTICS_SCRIPT_ATTRIBUTE = "data-riebeckite-analytics";
|
package/dist/src/script.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { AnalyticsOptions } from "./options.js";
|
|
2
|
-
/**
|
|
3
|
-
* Builds the JavaScript served from the analytics endpoint.
|
|
4
|
-
*
|
|
5
|
-
* Every configured value is JSON-encoded before it is embedded, so a value can
|
|
6
|
-
* never break out of the string literal that holds it. The one exception is
|
|
7
|
-
* `provider: "custom"` with a `snippet`, which is intentionally trusted and
|
|
8
|
-
* returned verbatim.
|
|
9
|
-
*/
|
|
10
|
-
export declare function buildAnalyticsScript(options: AnalyticsOptions): string;
|