Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ jobs:
- run: pnpm screenshot
- uses: actions/upload-artifact@v7
with:
name: emdash-openanalytics-screenshots
name: emdash-plugin-openanalytics-screenshots
path: docs/screenshots/*.png
if-no-files-found: error
98 changes: 54 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @blackswampai/emdash-openanalytics
# @blackswampai/emdash-plugin-openanalytics

Native EmDash CMS integration for OpenAnalytics, by Black Swamp AI.

Expand All @@ -7,28 +7,38 @@ overview inside EmDash. The admin page uses EmDash Block Kit controls, metric
cards, notices, a timeseries chart, Top Pages, and Traffic Sources. Private
credentials stay on the server.

## Installation
## Quick start

Requires EmDash 1.0.1 or later in the 1.x series and Node.js 22.16 or later.
This package has not been published to npm. For local testing, run `pnpm install`
and `pnpm build` in this checkout, then install it from your EmDash site:

```sh
pnpm add /path/to/emdash-openanalytics
```
1. Run `pnpm add @blackswampai/emdash-plugin-openanalytics` in your EmDash site.
2. Register `openAnalytics()` in the EmDash `plugins` array in `astro.config.mjs`.
3. Ensure your public layout renders `<EmDashHead page={page} />` inside `<head>`.
4. Deploy the site and set `EMDASH_ENCRYPTION_KEY` on its server.
5. Create an OpenAnalytics private read key and save it in this plugin's settings.
6. Open **OpenAnalytics** in plugin navigation; it validates once automatically,
then loads the overview. Revisit or choose a date range to see reports.

## Install

After an npm release is published, the installation command will be:
Install the package after the first public npm release is available:

> **No OpenAnalytics credential is needed to install or register this plugin.**
> **Do not put your `oa_sk_...` private read key in the npm install command,
> `astro.config.mjs`, or source code.** Add it after installation through
> OpenAnalytics plugin settings in EmDash. EmDash encrypts secret settings using
> `EMDASH_ENCRYPTION_KEY`. Keep it out of browser code and public environment variables.

```sh
pnpm add @blackswampai/emdash-openanalytics
pnpm add @blackswampai/emdash-plugin-openanalytics
```

Register the native plugin in `astro.config.mjs`:

```js
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { openAnalytics } from "@blackswampai/emdash-openanalytics";
import { openAnalytics } from "@blackswampai/emdash-plugin-openanalytics";

export default defineConfig({
integrations: [
Expand All @@ -48,37 +58,29 @@ a context created with `createPublicPageContext` from `emdash/page`.
Import `EmDashHead` from `emdash/ui`. Layouts without that insertion point cannot
render the tracker. See [EmDash's page fragment guide](https://docs.emdashcms.com/plugins/creating-native-plugins/page-fragments/).

## OpenAnalytics setup

1. Create a **private read key** in OpenAnalytics for the site you want to track.
Request `site:read` and `analytics:read`. Connection validation uses `site:read`;
the admin overview uses `analytics:read`.
Older keys may only have `site:read`; site validation cannot verify the extra scope.
2. Configure `EMDASH_ENCRYPTION_KEY` on the EmDash server **before saving a key**.
Follow [EmDash's secrets and key management guide](https://docs.emdashcms.com/deployment/secrets/).
3. Open **Plugins**, then the settings control for this plugin. Save the API URL,
private read key, tracking switch, and analytics timezone. Set the timezone
to your site's IANA timezone, such as `America/New_York`. It defaults to UTC;
EmDash's native plugin context does not expose the host site's timezone.
Tracking defaults to enabled, but no tracker appears before successful validation.
4. Open **OpenAnalytics** in EmDash's plugin navigation and click **Validate connection**.
The page shows the connected site, tracking readiness, API URL, and last
validation time. Use **Revalidate connection** after rotating tracker settings.

The existing protected validation route also remains available to administrators:
## Configure

```js
const response = await fetch("/_emdash/api/plugins/emdash-openanalytics/validate-connection", {
method: "POST",
headers: { "X-EmDash-Request": "1" },
});
console.log(await response.json());
```
1. Set `EMDASH_ENCRYPTION_KEY` on the EmDash server before saving a secret. See
[EmDash's secrets and key management guide](https://docs.emdashcms.com/deployment/secrets/).
2. In OpenAnalytics, create a private read key with `site:read` and `analytics:read`.
The first scope validates the site; the second allows the overview and reports.
3. In EmDash, open **Plugins** and the OpenAnalytics settings. Set the API URL,
private read key, tracking switch, and IANA analytics timezone (for example,
`America/New_York`; the default is UTC). Save settings.
4. Open **OpenAnalytics** in plugin navigation. With no matching saved snapshot,
the page validates once and then loads the overview. It shows the connected
site, tracking readiness, API URL, and last validation time.
5. If automatic validation fails, use **Retry connection**. The same failed
configuration will not trigger another automatic attempt. Changing settings
permits a fresh attempt. For an established connection, **Refresh connection**
validates separately from the date range control.

## Screenshots

These captures use real EmDash with synthetic analytics data.

The route requires `plugins:manage`. It reads the stored credential on the server;
you do not pass the key in this request. EmDash wraps the plugin's result in its
standard API response envelope. Connection failures contain safe error details.
Successful validation reports site identity, status, and tracker readiness.
![OpenAnalytics overview in EmDash](https://raw.githubusercontent.com/BlackSwampAI/emdash-plugin-openanalytics/dc749d963a528a0f2658380e764a99503eedf95c/docs/screenshots/openanalytics-overview.png)
![OpenAnalytics pages and traffic sources](https://raw.githubusercontent.com/BlackSwampAI/emdash-plugin-openanalytics/dc749d963a528a0f2658380e764a99503eedf95c/docs/screenshots/openanalytics-reports.png)

## Analytics overview

Expand Down Expand Up @@ -136,19 +138,25 @@ API URL or credential.

## Security

`oa_sk_…` is private and server-only. EmDash's `secret` setting encrypts it at
`oa_sk_…` is private and server-only. Never put it in `astro.config.mjs`, source
code, browser code, or public environment variables. Enter it only in the
OpenAnalytics plugin settings after configuring the host's `EMDASH_ENCRYPTION_KEY`.
EmDash's `secret` setting encrypts it at
rest and presents a write-only admin input; the plugin never puts it in public
HTML, validation results, or logs. Keep the EmDash encryption key available and
back it up according to the host's secret management practices.

`oa_pk_…` is the public browser tracking key. It belongs in page HTML. The plugin
`oa_pk_…` is the public browser tracking key. The plugin retrieves it
automatically from OpenAnalytics and puts it in page HTML. The plugin
uses EmDash's structured external-script fragment, which escapes attributes and
deduplicates the tracker by a stable fragment key.

The API client rejects redirects and uses a five-second timeout. API response
bodies and transport exceptions are not echoed into errors. The administrator
controls the API and tracker origins: configure endpoints you trust. HTTP works
for local self-hosted deployments; use HTTPS for production credentials.
for local self-hosted deployments. With a remote HTTP endpoint, the private key
travels without encryption; use HTTPS in production. The admin page warns when
the configured API URL uses remote HTTP.

## Self-hosting

Expand All @@ -159,7 +167,7 @@ it has no hard-coded hosted tracker or collector URL.

If any installation field is `null`, the connection can still validate, but no
script is emitted. Configure your OpenAnalytics deployment's `COLLECTOR_BASE_URL`
and a live public tracking key, then validate again. This scaffold does not add
and a live public tracking key, then validate again. The plugin does not add
manual tracker URL overrides.

## Current limitations
Expand All @@ -168,8 +176,10 @@ manual tracker URL overrides.
devices, sessions browser, individual visitors, custom-event reports, funnels,
revenue, web vitals, editor analytics, or realtime polling. OAuth and account
or site creation are also outside this plugin's current scope.
- Revalidate after tracker rotation or a collector URL change. Saved installation
- Refresh the connection after tracker rotation or a collector URL change. Saved installation
metadata has no automatic expiry; private-key revocation is detected on validation.
- Pre-release installations using the old `emdash-openanalytics` plugin ID must
re-enter their OpenAnalytics settings once after updating.
- Static pages receive the snapshot available when they are rendered. Rebuild
those pages after changing the connection or tracking switch.
- The tracker independently fetches OpenAnalytics's browser configuration. A
Expand Down
14 changes: 13 additions & 1 deletion demo/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# EmDash OpenAnalytics screenshot demo

Run `pnpm screenshot` from the repository root. It builds `dist/`, starts a local OpenAnalytics fixture and an actual EmDash Astro admin using the packaged plugin descriptor, authenticates through EmDash's development-only bypass, configures the fixture connection through the protected settings API, validates it, and captures three screenshots under `docs/screenshots/`.
Run `pnpm screenshot` from the repository root. It builds `dist/`, starts a local
OpenAnalytics fixture and an actual EmDash Astro admin using the packaged plugin
descriptor, authenticates through EmDash's development-only bypass, and opens
the plugin's blank analytics page. The harness fills and saves EmDash's native
settings form with the fixture API URL, synthetic private key, tracking switch,
and timezone. Saving makes no OpenAnalytics request. It then opens the analytics
page again, where the first site validation happens automatically, and captures overview, reports, and
narrow-layout screenshots under `docs/screenshots/`.

The demo asserts that the blank-settings page has no connection controls, the
first configured page visit makes exactly one site read before the four analytics
reads, and revisiting the page with unchanged settings makes no second site read.
There is no manual validation step in the setup.

The fixture and EmDash app bind to `127.0.0.1` on reserved ephemeral ports. Their readiness routes require a per-run random token, and EmDash also refuses to start if its configured port is occupied. The fixture accepts only its synthetic `oa_sk_demo_fixture_only_…` key. The EmDash demo database lives in a newly created temporary directory and is removed on exit. Captures are staged in another temporary directory; before they are copied into the repository, the harness scans rendered DOM text, page HTML, text/JSON browser responses, process output, and PNG bytes for private-key-shaped values and the fixture key. This environment has no OCR tool, so the screenshot check supplements the scan of the exact DOM used to take each image.

Expand Down
11 changes: 11 additions & 0 deletions docs/implementation-footprint.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ excluded. These are size comparisons, not runtime performance measurements.
| EmDash scaffold, PR #1 | 495 | 721 | 9 | 1 |
| EmDash native overview, PR #2 | 1458 | 1923 | 10 | 3 |
| EmDash pages and sources, PR #3 | 1770 | 2423 | 14 | 5 |
| EmDash 0.1.0 release candidate | 1903 | 2711 | 14 | 5 |
| n8n OpenAnalytics 0.1.1 | 607 | 613 | 5 | 11 |

PR #2 adds **963 production lines** and **1202 test lines**
Expand All @@ -26,6 +27,15 @@ The admin coordinator delegates range handling, connection blocks, overview/char
rendering, and report tables to four small modules. Successful sections remain
visible when another read fails.

The release candidate is `@blackswampai/emdash-plugin-openanalytics`, from
`BlackSwampAI/emdash-plugin-openanalytics`, with native plugin ID `openanalytics`.
Release hardening adds automatic first-load validation, connection controls apart
from the date range, bounded upstream responses and route bodies, package checks,
and a form-driven screenshot setup. It adds no analytics reports or endpoints.
A fresh connection adds one site read before the existing four analytics reads;
matching snapshots skip that site read on revisits and range changes. Failed
configuration fingerprints require an explicit connection retry.

The EmDash plugin now owns a native admin page, public tracker insertion,
credential-bound installation snapshots, secure read transport, response
validation/projection, and useful connection/error/freshness states. The n8n
Expand Down Expand Up @@ -76,3 +86,4 @@ size. EmDash PR #3 packs to **about 23.7 kB** (decimal), compared with
Its package includes compiled workflow operations, source maps, and icons; the
EmDash package includes the compiled plugin, declarations, README, and two
contract/footprint documents. Package size reflects those different contents.
The current 0.1.0 release candidate packs to approximately **26.5 kB** (decimal).
Binary file modified docs/screenshots/openanalytics-narrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/openanalytics-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/openanalytics-reports.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions docs/upstream-contracts.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
# Verified upstream contracts

## Release naming and current native-plugin contract (2026-09-29)

Before the release naming change, I rechecked the current EmDash upstream
`main` ref with `git ls-remote`: `54209bc9bd0b48e12bdefa8ac971da01ced7990f`.
This is the pinned upstream revision for the release-convention check. A direct
source clone was unavailable, so I used GitHub's public API to read the relevant
source files at this exact commit.

The current [native plugin distribution guide](https://docs.emdashcms.com/plugins/creating-native-plugins/distributing/)
explicitly says `definePlugin()` accepts a simple unscoped ID of lowercase
letters, digits, and hyphens, and recommends that form because an ID occupies
one route path segment. It separately shows a scoped npm entrypoint. Therefore
`@blackswampai/emdash-plugin-openanalytics` as the npm package and `openanalytics`
as the native plugin ID follow the upstream recommendation; the package scope
does not need to be repeated in the plugin ID. Current EmDash packages such as
`@emdash-cms/plugin-audit-log` also use `plugin-` in their package name, while
the official distribution guide documents that package identity and plugin ID
are distinct values. No current EmDash technical constraint argues against the
requested Black Swamp AI package family. At the pinned revision,
[`packages/plugins/audit-log/package.json`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/plugins/audit-log/package.json)
is named `@emdash-cms/plugin-audit-log`; the native Color example uses
`id: "color"` in
[`packages/plugins/color/src/index.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/plugins/color/src/index.ts).
The route-segment recommendation appears in the
[current native distribution guide](https://docs.emdashcms.com/plugins/creating-native-plugins/distributing/).

The current [native generated-settings documentation](https://docs.emdashcms.com/plugins/creating-native-plugins/react-admin/)
describes `admin.settingsSchema` as the generated form contract. The [official
hook reference](https://docs.emdashcms.com/reference/hooks/) documents plugin
lifecycle, content, media, and public-page hooks; it does not define a settings
save callback. Likewise, the current [hook guide](https://docs.emdashcms.com/plugins/creating-plugins/hooks/)
says hooks are declared at plugin definition time, with `content:afterSave`
being explicitly a content-save hook. There is no documented native settings
`afterSave`/`onSave` extension point to validate credentials after a generated
settings form save. The pinned handler source,
[`packages/core/src/api/handlers/plugin-settings.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/core/src/api/handlers/plugin-settings.ts),
shows `handlePluginSettingsUpdate` validating, encrypting, transactionally
writing, and reading back declared keys; it takes no plugin callback and
dispatches no settings lifecycle event. Settings are namespaced as
`plugin:{pluginId}:settings:{key}` in
[`packages/core/src/plugins/settings.ts`](https://github.com/emdash-cms/emdash/blob/54209bc9bd0b48e12bdefa8ac971da01ced7990f/packages/core/src/plugins/settings.ts).
Connection establishment therefore belongs in the authenticated plugin admin
page flow, rather than relying on undocumented host internals.

GitHub's current [repository rename documentation](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository)
confirms that ordinary web URLs and `git clone`, `git fetch`, and `git push`
requests to the old repository location redirect after a rename. Project Pages
URLs are the exception; GitHub Actions hosted from a renamed repository also do
not redirect. The documented conditions include having organization-owner or
repository-admin permission and not recreating a repository under the old name.
Thus a normal rename from `BlackSwampAI/emdash-openanalytics` to
`BlackSwampAI/emdash-plugin-openanalytics` preserves ordinary repository and
git traffic, subject to those stated exceptions.

The repository rename was performed after the release branch was committed and
pushed. Both the old and new GitHub API paths resolve to repository ID
`1396756742` and the canonical name `BlackSwampAI/emdash-plugin-openanalytics`;
the old repository path redirects to the renamed repository.

Inspected on 2026-09-29 before implementation:

- EmDash [`54209bc9bd0b48e12bdefa8ac971da01ced7990f`](https://github.com/emdash-cms/emdash/tree/54209bc9bd0b48e12bdefa8ac971da01ced7990f),
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@blackswampai/emdash-openanalytics",
"name": "@blackswampai/emdash-plugin-openanalytics",
"version": "0.1.0",
"description": "Native EmDash plugin for OpenAnalytics",
"keywords": [
Expand All @@ -8,15 +8,15 @@
"emdash-plugin",
"openanalytics"
],
"homepage": "https://github.com/BlackSwampAI/emdash-openanalytics#readme",
"homepage": "https://github.com/BlackSwampAI/emdash-plugin-openanalytics#readme",
"bugs": {
"url": "https://github.com/BlackSwampAI/emdash-openanalytics/issues"
"url": "https://github.com/BlackSwampAI/emdash-plugin-openanalytics/issues"
},
"license": "MIT",
"author": "Black Swamp AI",
"repository": {
"type": "git",
"url": "git+https://github.com/BlackSwampAI/emdash-openanalytics.git"
"url": "git+https://github.com/BlackSwampAI/emdash-plugin-openanalytics.git"
},
"files": [
"dist",
Expand All @@ -32,6 +32,9 @@
"import": "./dist/index.mjs"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsdown src/index.ts --format esm --dts --clean",
"dev": "tsdown src/index.ts --format esm --dts --watch",
Expand Down Expand Up @@ -60,7 +63,7 @@
"react-dom": "19.3.0",
"tsdown": "0.20.3",
"typescript": "5.9.3",
"vitest": "4.0.15"
"vitest": "4.1.11"
},
"peerDependencies": {
"emdash": ">=1.0.1 <2"
Expand Down
Loading
Loading