@riebeckite/plugin-analytics 0.0.1 → 0.0.5

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 CHANGED
@@ -1,136 +1,97 @@
1
1
  # @riebeckite/plugin-analytics
2
2
 
3
- Provider analytics injection for Riebeckite sites: a static bootstrap endpoint
4
- plus a browser entry that loads it.
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
- ## Overview
8
+ ## Design
9
9
 
10
- `analytics()` registers:
11
-
12
- - one GET endpoint (default `/_analytics.js`) that serves a small provider
13
- bootstrap written in JavaScript, and
14
- - one client entry, `initAnalytics`, that appends
15
- `<script defer src="/_analytics.js">` to `document.head` exactly once.
16
-
17
- The plugin is build-time only. It never executes provider code during the static
18
- build.
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 { defineConfig } from "@riebeckite/core";
24
- import { analytics } from "@riebeckite/plugin-analytics";
23
+ import { analytics, MemoryAnalyticsProvider } from "@riebeckite/plugin-analytics";
24
+
25
+ const provider = new MemoryAnalyticsProvider();
25
26
 
26
- export default defineConfig({
27
- // ...
27
+ export default {
28
28
  plugins: [
29
- analytics({ provider: "plausible", domain: "example.com" }),
29
+ analytics({
30
+ provider,
31
+ publicConfig: { collectorUrl: "/analytics/events" },
32
+ }),
30
33
  ],
31
- });
32
- ```
33
-
34
- Provider examples:
35
-
36
- ```ts
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;" });
34
+ };
43
35
  ```
44
36
 
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.
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.
59
40
 
60
- Provider script URL defaults:
41
+ ## Browser behavior
61
42
 
62
- | Provider | Default URL |
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` |
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.
67
46
 
68
- ## Endpoint contract
47
+ In a browser, the initializer sends exactly one event per document:
69
48
 
70
- - `GET /_analytics.js`
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.
77
-
78
- `google-analytics` also defines `window.dataLayer` and `window.gtag` before
79
- configuring the measurement id. `custom` returns the `snippet` verbatim, or a
80
- generic loader for `scriptUrl`.
81
-
82
- ## Client behavior (`initAnalytics`)
83
-
84
- - Runs on page load through `initRiebeckiteClient()`; no component or layout
85
- change is required.
86
- - Appends one `<script defer src="/_analytics.js" data-riebeckite-analytics>`
87
- to `document.head`, and is a no-op if that tag already exists.
88
-
89
- ## Privacy and trust boundary
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
+ ```
90
58
 
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.
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.
97
63
 
98
- ## Verifiability
64
+ Content IDs are an authoring responsibility. Duplicate or conflicting `id`/`uid`
65
+ declarations across published notes are reported by
66
+ [`@riebeckite/plugin-diagnostics`](../plugins/diagnostics/README.md)
67
+ (`duplicate-content-id` / `invalid-content-id`) and validated by the plugin's own
68
+ `validateAnalyticsOptions` at `check` time.
99
69
 
100
- Static builds do not execute client JavaScript. To keep analytics verifiable in
101
- built HTML, `onManifestCreated` appends functional tags to every entry:
70
+ ## Provider contract
102
71
 
103
- ```html
104
- <link rel="preload" as="script" href="/_analytics.js" />
105
- <script defer src="/_analytics.js" data-riebeckite-analytics></script>
72
+ ```ts
73
+ const result = await provider.query({
74
+ type: "popular_content",
75
+ limit: 10,
76
+ timeRange: { from: "2026-01-01T00:00:00.000Z" },
77
+ });
106
78
  ```
107
79
 
108
- These are the real tags the browser would load, so no fabricated marker is
109
- inserted. The injection is idempotent (guarded by `data-riebeckite-analytics`)
110
- and does not alter the surrounding document structure.
80
+ Capabilities are `capture`, `content_page_views`, and `popular_content`. Call
81
+ `assertAnalyticsQuerySupported(provider, query)` when implementing a provider
82
+ that may not support all queries.
111
83
 
112
- ## Limitations
84
+ ## Diagnostics
113
85
 
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.
86
+ `@riebeckite/plugin-diagnostics` reports an `analytics-untracked` finding for
87
+ published content without a stable content ID when the site enables this
88
+ plugin, so tracking gaps are visible in `check`, `doctor`, and build
89
+ diagnostics.
123
90
 
124
91
  ## Exports
125
92
 
126
- - `analytics(options)` / `analyticsPlugin(options)` — plugin factory
127
- - `initAnalytics` — browser initializer (also via
128
- `@riebeckite/plugin-analytics/client`)
129
- - `buildAnalyticsScript(options)` — pure bootstrap builder
130
- - `validateAnalyticsOptions(options)` — options validator
131
- - Constants: `ANALYTICS_SCRIPT_PATH`, `ANALYTICS_SCRIPT_ATTRIBUTE`
132
- - Types: `AnalyticsOptions`, `AnalyticsProvider`
133
-
134
- ## See also
135
-
136
- - [Plugin guide](../../docs/plugins_en.md)
93
+ - `analytics()` / `analyticsPlugin()`
94
+ - `initAnalytics` (`@riebeckite/plugin-analytics/client`)
95
+ - `MemoryAnalyticsProvider`
96
+ - Event, query/result, provider/capability, and public-config types
97
+ - `UnsupportedAnalyticsQueryError` and capability helpers
package/README_ja.md CHANGED
@@ -1,113 +1,59 @@
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
- `analytics()` は次の2つを登録します。
10
-
11
- - プロバイダのブートストラップ JavaScript を返す GET エンドポイント(既定は `/_analytics.js`)
12
- - `document.head` に `<script defer src="/_analytics.js">` を一度だけ追加するクライアントエントリ `initAnalytics`
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 { defineConfig } from "@riebeckite/core";
20
- import { analytics } from "@riebeckite/plugin-analytics";
17
+ import { analytics, MemoryAnalyticsProvider } from "@riebeckite/plugin-analytics";
21
18
 
22
- export default defineConfig({
23
- // ...
19
+ export default {
24
20
  plugins: [
25
- analytics({ provider: "plausible", domain: "example.com" }),
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` |
29
+ `collectorUrl` は公開してよい収集先 URL です。相対パスまたは HTTP(S) URL を指定し、JSON の `POST` を受け付ける収集側を用意します。将来のプロバイダパッケージは、非公開ランタイム設定を使ってその収集側を提供できます。
62
30
 
63
- ## エンドポイントの契約
31
+ ## ブラウザでの動作
64
32
 
65
- - `GET /_analytics.js`
66
- - `200 OK`
67
- - `Content-Type: application/javascript; charset=utf-8`
68
- - `Cache-Control: public, max-age=3600`
69
- - 本文: プロバイダの `<script>` タグ(必要に応じて `async` / `defer`)を生成する即時実行スニペット。設定値はすべて JSON エンコードされるため、文字列リテラルを壊すことはできません。
33
+ Core の安定コンテンツ ID(フロントマターの `id`、互換用の `uid`)がある公開コンテンツにだけ識別子マーカーを出力します。汎用 `publicConfig` 経由で登録された `initAnalytics` は、ブラウザでドキュメントごとに一度だけ次のようなイベントを送ります。
70
34
 
71
- `google-analytics` は測定 ID を設定する前に `window.dataLayer` と `window.gtag` を定義します。`custom` は `snippet` をそのまま返すか、`scriptUrl` 用の汎用ローダーを返します。
72
-
73
- ## クライアントの挙動(`initAnalytics`)
74
-
75
- - `initRiebeckiteClient()` 経由でページ読み込み時に実行されます。コンポーネントやレイアウトの変更は不要です。
76
- - `<script defer src="/_analytics.js" data-riebeckite-analytics>` を `document.head` に1つ追加します。すでに存在する場合は何もしません。
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
- これはブラウザが実際に読み込むタグそのもので、検証用のダミーマーカーは挿入しません。付与は冪等(`data-riebeckite-analytics` で判定)で、周囲の HTML 構造を変えません。
45
+ `path` と `lang` は補助情報であり、ID ではありません。安定 ID のないコンテンツは誤って計測しません。ビルド時・SSR 時は何もせず、同じドキュメント内での初期化も冪等です。現行リポジトリは静的なドキュメント遷移のため SPA フックは追加しておらず、SPA 遷移は自動計測しません。
94
46
 
95
- ## 制限事項
47
+ コンテンツ ID は執筆側の責務です。公開ノート間での `id`/`uid` の重複・抵触は [`@riebeckite/plugin-diagnostics`](../plugins/diagnostics/README_ja.md) が報告し(`duplicate-content-id` / `invalid-content-id`)、プラグイン自身の `validateAnalyticsOptions` が `check` 時に検証します。
96
48
 
97
- - **クライアント側のパスは固定です。** クライアントエントリはオプションを受け取らないため、常に `/_analytics.js` を読み込みます。`scriptPath` を変更する場合は、エンドポイントのマウントと読み込みを独自のクライアントエントリで行ってください。
98
- - **SPA のルート遷移には対応しません。** クライアントサイドのルーティングによる画面遷移は自動では計測されません。プロバイダ側で設定するか、手動でイベントを送信してください。
99
- - **ドキュメントのライフサイクルは初回読み込みのみです。** より複雑なライフサイクルが必要な場合は独自の `snippet` を用意してください。
100
- - **同意管理は行いません。** 法令上必要な場合は、クライアントエントリ側で同意を得るまで読み込みを止めてください。
49
+ ## 診断
101
50
 
102
- ## 主なエクスポート
51
+ `@riebeckite/plugin-diagnostics` は、サイトで本プラグインが有効なとき、安定 content ID を持たない公開コンテンツを `analytics-untracked` として報告します。計測の抜けを `check` / `doctor` / build の診断で確認できます。
103
52
 
104
- - `analytics(options)` / `analyticsPlugin(options)`: プラグインを作成する
105
- - `initAnalytics`: クライアント側の初期化(`@riebeckite/plugin-analytics/client` からも利用可)
106
- - `buildAnalyticsScript(options)`: ブートストラップを生成する純粋関数
107
- - `validateAnalyticsOptions(options)`: オプションの検証
108
- - 定数: `ANALYTICS_SCRIPT_PATH`、`ANALYTICS_SCRIPT_ATTRIBUTE`
109
- - 型: `AnalyticsOptions`、`AnalyticsProvider`
110
-
111
- ## 関連資料
53
+ ## 主なエクスポート
112
54
 
113
- - [プラグインガイド](../../../docs/ja/plugin-system.md)
55
+ - `analytics()` / `analyticsPlugin()`
56
+ - `initAnalytics`(`@riebeckite/plugin-analytics/client`)
57
+ - `MemoryAnalyticsProvider`
58
+ - イベント、クエリ/結果、プロバイダ/機能、公開設定の型
59
+ - `UnsupportedAnalyticsQueryError` と機能確認ヘルパー
package/dist/client.d.ts CHANGED
@@ -1,3 +1,2 @@
1
- export { ANALYTICS_SCRIPT_PATH } from "./src/constants.js";
2
- export { initAnalytics } from "./src/init.js";
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
- function initAnalytics() {
7
- if (typeof document === "undefined") return;
8
- if (document.querySelector(`script[${ANALYTICS_SCRIPT_ATTRIBUTE}]`)) return;
9
- const script = document.createElement("script");
10
- script.defer = true;
11
- script.src = ANALYTICS_SCRIPT_PATH;
12
- script.setAttribute(ANALYTICS_SCRIPT_ATTRIBUTE, "");
13
- document.head.appendChild(script);
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
- ANALYTICS_SCRIPT_PATH,
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 { ANALYTICS_SCRIPT_ATTRIBUTE, ANALYTICS_SCRIPT_PATH, } from "./src/constants.js";
3
- export { initAnalytics } from "./src/init.js";
4
- export type { AnalyticsOptions, AnalyticsProvider } from "./src/options.js";
5
- export { ANALYTICS_PROVIDERS, validateAnalyticsOptions } from "./src/options.js";
6
- export { buildAnalyticsScript } from "./src/script.js";
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
- * Injects a provider analytics bootstrap into every page.
9
- *
10
- * The provider snippet is served from a static endpoint (`/_analytics.js` by
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<AnalyticsOptions>;
17
- /** Alias kept for symmetry with the other `*Plugin` factories. */
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/constants.ts
10
- var ANALYTICS_SCRIPT_PATH = "/_analytics.js";
11
- var ANALYTICS_SCRIPT_ATTRIBUTE = "data-riebeckite-analytics";
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: "measurementId",
43
- message: "google-analytics requires a measurement id."
41
+ path: "provider",
42
+ message: "Expected an analytics provider."
44
43
  });
45
44
  }
46
- if (options.provider === "custom" && !hasText(options.snippet) && !hasText(options.scriptUrl)) {
45
+ if (!options?.publicConfig || !isCollectorUrl(options.publicConfig.collectorUrl)) {
47
46
  issues.push({
48
- path: "snippet",
49
- message: "custom requires a snippet or a scriptUrl."
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 isProvider(value) {
61
- return typeof value === "string" && ANALYTICS_PROVIDERS.includes(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 hasText(value) {
67
- return typeof value === "string" && value.trim().length > 0;
68
- }
69
- function validateOptionalString(value, path, issues) {
70
- if (value !== void 0 && typeof value !== "string") {
71
- issues.push({ path, message: "Expected a string." });
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/script.ts
79
- var PLAUSIBLE_SCRIPT_URL = "https://plausible.io/js/script.js";
80
- var UMAMI_SCRIPT_URL = "https://cloud.umami.is/script.js";
81
- var GOOGLE_ANALYTICS_SCRIPT_URL = "https://www.googletagmanager.com/gtag/js";
82
- function buildAnalyticsScript(options) {
83
- switch (options.provider) {
84
- case "plausible":
85
- return buildPlausibleScript(options);
86
- case "umami":
87
- return buildUmamiScript(options);
88
- case "google-analytics":
89
- return buildGoogleAnalyticsScript(options);
90
- case "custom":
91
- return buildCustomScript(options);
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 buildPlausibleScript(options) {
97
- const domain = options.domain ?? "";
98
- const url = options.scriptUrl ?? PLAUSIBLE_SCRIPT_URL;
99
- return [
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
- function buildCustomScript(options) {
155
- if (options.snippet?.trim()) {
156
- return `${options.snippet.trimEnd()}
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
- const url = options.scriptUrl ?? "";
160
- return [
161
- "/* @riebeckite/plugin-analytics: custom */",
162
- "(function () {",
163
- ' var script = document.createElement("script");',
164
- " script.defer = true;",
165
- ` script.src = ${jsString(url)};`,
166
- " document.head.appendChild(script);",
167
- "})();",
168
- ""
169
- ].join("\n");
170
- }
171
- function jsString(value) {
172
- const encoded = JSON.stringify(value);
173
- return encoded === void 0 ? '""' : encoded;
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
- // src/init.ts
177
- function initAnalytics() {
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
- endpoints: [
196
- defineEndpoint(
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
- clientEntries: [createClientEntry("analytics", "initAnalytics")],
208
- onPostProcessed: (context) => {
209
- tracked.set(context.slug, context.content);
148
+ onPostProcessed: ({ slug, content }) => {
149
+ tracked.set(slug, content);
210
150
  },
211
- onManifestCreated: (context) => {
212
- const tags = buildAnalyticsTags(scriptPath);
213
- for (const entry of context.manifest.entries) {
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 buildAnalyticsTags(scriptPath) {
221
- const href = escapeHtmlAttribute(scriptPath);
222
- return [
223
- `<link rel="preload" as="script" href="${href}" />`,
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
- ${tags}`;
164
+ ${marker}`;
231
165
  }
232
- if (content && !content.html.includes(ANALYTICS_SCRIPT_ATTRIBUTE)) {
166
+ if (content && !content.html.includes(ANALYTICS_CONTENT_ID_ATTRIBUTE)) {
233
167
  content.html = `${content.html}
234
- ${tags}`;
168
+ ${marker}`;
235
169
  }
236
170
  }
237
171
  export {
238
- ANALYTICS_PROVIDERS,
239
- ANALYTICS_SCRIPT_ATTRIBUTE,
240
- ANALYTICS_SCRIPT_PATH,
172
+ ANALYTICS_CAPABILITIES,
173
+ ANALYTICS_CONTENT_ID_ATTRIBUTE,
174
+ MemoryAnalyticsProvider,
175
+ UnsupportedAnalyticsQueryError,
241
176
  analytics,
242
177
  analyticsPlugin,
243
- buildAnalyticsScript,
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;
@@ -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
- * Injects the analytics bootstrap `<script>` into `document.head` exactly once.
3
- * The bootstrap endpoint then loads the configured provider script.
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
+ }
@@ -1,32 +1,16 @@
1
1
  import type { ConfigValidationIssue } from "@riebeckite/core";
2
- /** Analytics providers supported by the built-in bootstrap. */
3
- export declare const ANALYTICS_PROVIDERS: readonly ["plausible", "umami", "google-analytics", "custom"];
4
- export type AnalyticsProvider = (typeof ANALYTICS_PROVIDERS)[number];
5
- export type AnalyticsOptions = {
6
- /** Provider whose bootstrap is served. */
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
- /** Plausible domain, or optional Umami `data-domains` value. */
9
- domain?: string;
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.1",
3
+ "version": "0.0.5",
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.1"
23
+ "@riebeckite/core": "0.0.5"
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": "Analytics provider integration for Riebeckite sites.",
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
  }
@@ -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";
@@ -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;