Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - TryGhost/TrafficAnalytics: Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics. · GitHub
Skip to content

Repository files navigation

Traffic Analytics

Traffic Analytics Service - A web analytics proxy for Ghost that processes and enriches traffic data before forwarding it to Tinybird's analytics API.

How it Works

The following sequence diagram shows a simplified overview of where the Analytics Service fits in to Ghost's traffic analytics features.

  1. A user requests a Ghost site's homepage (or any other page on the site's frontend)
  2. Ghost serves the page's HTML, plus a script called ghost-stats.js
  3. The ghost-stats.js script executes and sends a POST request to the Analytics Service's POST /api/v1/page_hit endpoint
  4. The Analytics Service receives the request and processes it. This includes parsing the user agent, generating a user signature, etc.
  5. The Analytics Service proxies the request to Tinybird
  6. Tinybird receives the request and stores it in its Clickhouse database
  7. The Analytics Service then proxies the response from Tinybird back to the user's browser.
sequenceDiagram
autonumber
participant User as User Browser
participant Ghost as Ghost Site
participant AS as Analytics Service
participant TB as Tinybird
User->>Ghost: GET /
activate Ghost
Ghost-->>User: HTML + ghost-stats.js
deactivate Ghost
Note over User: ghost-stats.js executes
User->>+AS: POST /api/v1/page_hit
AS->>AS: Process Request
AS->>+TB: POST /v0/events<br/>Enriched analytics data
TB-->>-AS: 202 Accepted
AS-->>-User: 202 Accepted
Note over User,TB: Analytics event successfully tracked
Loading

Run modes

The "Process Request" and "forward to Tinybird" steps above happen in one of two ways, and this is how the service runs by default in development and production:

  • Batch mode (default) — The ingest service validates the request, filters bot traffic, publishes non-bot raw events to a Google Cloud Pub/Sub topic, and immediately returns 202. A separate worker process consumes from the subscription, enriches each event (user-agent parsing, referrer parsing, user signature), batches events, and forwards them to Tinybird's /v0/events endpoint. This decouples request handling from Tinybird ingestion. Started with yarn dev (alias for yarn dev:batch).
  • Proxy mode (synchronous) — With no Pub/Sub topic configured, the ingest service filters bot traffic, enriches non-bot requests inline, and proxies them straight to Tinybird in the same request/response cycle. Started with yarn dev:proxy.

Both modes run from the same image; the role is selected by the WORKER_MODE environment variable (worker vs. ingest) and the presence of PUBSUB_TOPIC_PAGE_HITS_RAW (batch vs. proxy). See docs/architecture.md for a diagram and full detail.

Features

  • User agent parsing for OS, browser, and device detection
  • Referrer URL parsing and categorization
  • Privacy-preserving user signatures with daily-rotating salts

Configuration

Copy .env.example to .env and configure as needed. Set ENABLE_BOT_DETECTION_HEADER=true to include x-ghost-bot-detected: true on 202 responses for filtered bots; this response header is omitted by default. For local development with Ghost, see Develop locally with Ghost

Develop

Pre-requisites:

  • A container runtime, such as Docker Desktop or Orbstack
  • Docker Compose
  1. git clone this repo & cd into it as usual
  2. yarn dev to build & start all required development services. The Analytics Service will be reachable at http://localhost:3000.

Develop locally with Ghost

If you want to manually test the Analytics Service + Ghost together locally, there are just a few more steps to follow. You'll need this repo and TryGhost/Ghost cloned locally.

  1. In Ghost, run pnpm dev:analytics:local. This starts Ghost, tinybird-local, and the published analytics image, while routing /.ghost/analytics/** requests to the stable network alias provided by this repository. The published analytics container remains running, but the gateway sends page hits to this local checkout instead.
  2. In this repo, run yarn dev:ghost. This starts the batch-mode ingest service and worker, joins them to Ghost's Docker network, and mounts its shared-config volume so they can reach tinybird-local with the generated tracker token.

Now when you visit Ghost at http://localhost:2368, you should see requests to /.ghost/analytics/api/v1/page_hit in this repository's logs, and the worker will send those events to the tinybird-local service running in the Ghost project.

Test

  • yarn test:types — run Typescript typechecks in Docker
  • yarn test:unit — run all unit tests in Docker
  • yarn test:integration — run all integration tests in Docker
  • yarn test — run typechecks, unit tests and integration tests in Docker
  • yarn test:e2e — run e2e tests (with wiremock) in Docker

Lint

  • yarn lint run eslint in docker compose

Multi-Worktree Development

This project supports running multiple worktrees simultaneously using Docker Compose. Each worktree can run its own isolated development environment with unique ports and container names.

Setup

  1. Create worktrees as usual with git worktree
  2. Configure each worktree with a unique .env file:
# main worktree (.env) - uses defaults
NODE_ENV=development
# work worktree (.env) 
NODE_ENV=development
COMPOSE_PROJECT_NAME=traffic-analytics-work
ANALYTICS_PORT=3001
FIRESTORE_PORT=8081
# scratch worktree (.env)
NODE_ENV=development COMPOSE_PROJECT_NAME=traffic-analytics-scratch
ANALYTICS_PORT=3002
FIRESTORE_PORT=8082

Usage

Each worktree runs completely isolated:

  • Unique ports: No conflicts between worktrees
  • Isolated containers: Auto-generated names like traffic-analytics-work-analytics-service-1
  • Separate volumes: Each worktree has its own node_modules volume
  • Independent projects: Services can run simultaneously
# Start development in any worktreecd /path/to/worktree
docker compose up
# Each worktree accessible on its configured port# main: http://localhost:3000# work: http://localhost:3001 # scratch: http://localhost:3002

Deployment

Development Workflow

  1. Create a branch and make your changes
  2. Open a PR against main
  3. Optionally test on staging by adding the deploy-staging label to your PR
    • This deploys your branch to staging without merging
    • The label is automatically removed after deployment
  4. Merge to main when ready

What happens on merge

When a PR is merged to main, the following happens automatically:

  1. Version bump — The patch version is automatically incremented (e.g., 1.2.3 → 1.2.4)
  2. Tag creation — A git tag is created and pushed (e.g., v1.2.4)
  3. Docker Hub — The image is published to Docker Hub
  4. Deploy to staging and production — Both environments are deployed in parallel
  5. Health checks — Automated health checks run against both environments
  6. Slack notification — The team is notified of the new release

Manual deployment

You can manually trigger a deployment via the GitHub Actions UI by running the "Deploy" workflow with workflow_dispatch.

For the full pipeline (version bump, image build/push to GCP Artifact Registry, Docker Hub release, Cloud Run deploy, health checks, Slack notifications, and rollback), see docs/deployment.md.

Documentation

  • docs/architecture.md — run modes (batch vs. proxy), the Pub/Sub pipeline, salt-store adapters, OpenTelemetry, and the worker.
  • docs/deployment.md — CI and deployment pipeline, staging/production, deploy-staging label, and rollback.

Copyright & License

Copyright (c) 2013-2026 Ghost Foundation - Released under the MIT license.

About

Analytics Service that sits between Ghost and Tinybird for Ghost's native web analytics.

Resources

Security policy

Stars

11 stars

Watchers

4 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages