Installation Overview
Pirsch supports three tracking methods, each with different trade-offs:
| Method | Ad Blocker Proof | Real-Time Dashboard | Implementation Effort |
|---|---|---|---|
Client-side script (pirsch.js) |
No | Yes | Minimal (one script tag) |
| Server-side SDK (Go, Node, PHP, Python) | Yes | No | Moderate (middleware integration) |
| Proxied client-side script | Yes | Yes | Moderate (reverse proxy config) |
Most implementations use the client-side script. Add server-side tracking or a proxy if your audience has high ad blocker usage (developer/technical audiences typically run 30-50% ad blocker rates).
Client-Side Script Installation
Basic Installation
Add the script inside <head>. The defer attribute ensures it loads after HTML parsing without blocking rendering.
<head>
<!-- Pirsch: pageview tracking only -->
<script defer src="https://api.pirsch.io/pirsch.js"
id="pirschjs"
data-code="YOUR_IDENTIFICATION_CODE"></script>
</head>
To also track custom events, use the extended script instead:
<head>
<!-- Pirsch: pageview + event tracking -->
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_IDENTIFICATION_CODE"></script>
</head>
Get your identification code from the Pirsch dashboard: Settings > Integration > Identification Code.
Excluding Development Environments
Use data-dev to prevent tracking on local and staging environments:
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"
data-dev="localhost,127.0.0.1,staging.example.com,*.test"></script>
The script checks window.location.hostname against each entry. Wildcards are supported.
Verifying the Installation
Open your site and check the browser DevTools Network tab. Filter for pirsch.io. You should see:
- A GET request for
pirsch.jsorpirsch-extended.js(the script itself) - A POST request to
api.pirsch.io/api/v1/hit(the pageview hit)
The POST should return HTTP 200. If it returns 400, the identification code is wrong. If you see no requests at all, an ad blocker or CSP is blocking the script.
In the Pirsch dashboard, your visit should appear within 5-10 minutes under the real-time counter.
// Quick console check
console.log(document.getElementById('pirschjs') ? 'Script tag present' : 'Missing');
console.log(typeof pirsch !== 'undefined' ? 'pirsch object loaded' : 'Not loaded yet');
Platform-Specific Installation
WordPress
Add via your theme's functions.php:
function add_pirsch_tracking() {
?>
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="<?php echo esc_attr(get_option('pirsch_code')); ?>"></script>
<?php
}
add_action('wp_head', 'add_pirsch_tracking');
Or use the official Pirsch WordPress plugin, which adds the script automatically and provides a settings page for the identification code.
Next.js (App Router)
Add the script in your root layout:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<Script
src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}
Track client-side route changes with a component:
// components/PirschPageview.tsx
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
declare global {
interface Window {
pirsch?: { hit: () => void };
}
}
export function PirschPageview() {
const pathname = usePathname();
useEffect(() => {
// Skip the first render -- pirsch.js handles the initial pageview
if (window.pirsch?.hit) {
window.pirsch.hit();
}
}, [pathname]);
return null;
}
Include <PirschPageview /> in your root layout's <body>.
Astro
Add the script in your base layout:
---
// layouts/Layout.astro
---
<html lang="en">
<head>
<script
defer
src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"
is:inline
></script>
</head>
<body>
<slot />
</body>
</html>
The is:inline directive prevents Astro from bundling the script, keeping it as a direct reference to the Pirsch CDN.
Shopify
Edit your theme's theme.liquid file:
- Go to Online Store > Themes > Edit Code
- Open
layout/theme.liquid - Add before
</head>:
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"></script>
Hugo / Jekyll / Static Site Generators
Add the script tag to your base template (Hugo: layouts/_default/baseof.html, Jekyll: _layouts/default.html):
<head>
{{ partial "head.html" . }}
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"></script>
</head>
Google Tag Manager
Create a Custom HTML tag:
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"></script>
Set the trigger to All Pages. Note that loading Pirsch through GTM means visitors with ad blockers that block GTM will also lose Pirsch tracking. For ad blocker resilience, install the script directly.
Server-Side SDK Installation
Server-side tracking sends hits from your backend. The visitor never communicates with api.pirsch.io directly, making it invisible to ad blockers.
Go SDK
go get github.com/pirsch-analytics/pirsch-go-sdk/v2
package main
import (
"log"
"net/http"
pirsch "github.com/pirsch-analytics/pirsch-go-sdk/v2"
)
var client = pirsch.NewClient(
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
nil, // uses default Pirsch API endpoint
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Send pageview hit -- SDK reads IP and User-Agent from the request
if err := client.Hit(r); err != nil {
log.Printf("pirsch error: %v", err)
}
// Serve your page
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<html><body>Hello</body></html>"))
})
// Track a custom event
http.HandleFunc("/api/purchase", func(w http.ResponseWriter, r *http.Request) {
err := client.Event(r, "Purchase Complete", 0, map[string]string{
"plan": "Pro",
"value": "149.99",
})
if err != nil {
log.Printf("pirsch event error: %v", err)
}
w.WriteHeader(http.StatusOK)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
The SDK handles OAuth2 token management internally. It requests a token on the first call and refreshes it automatically before expiry.
Node.js SDK
npm install pirsch-sdk
const express = require('express');
const { Pirsch } = require('pirsch-sdk');
const app = express();
const pirschClient = new Pirsch({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET'
});
// Middleware: track every page request
app.use(async (req, res, next) => {
// Skip static assets
if (req.path.match(/\.(js|css|png|jpg|svg|ico|woff2?)$/)) {
return next();
}
try {
await pirschClient.hit({
url: `https://example.com${req.path}`,
ip: req.ip,
user_agent: req.headers['user-agent'] || '',
accept_language: req.headers['accept-language'] || '',
referrer: req.headers['referer'] || ''
});
} catch (err) {
console.error('pirsch hit error:', err.message);
}
next();
});
// Track a custom event
app.post('/api/subscribe', async (req, res) => {
await pirschClient.event({
event_name: 'Newsletter Subscribe',
url: `https://example.com${req.path}`,
ip: req.ip,
user_agent: req.headers['user-agent'] || '',
event_meta: {
source: req.body.source || 'unknown'
}
});
res.json({ ok: true });
});
app.listen(3000);
PHP
<?php
require_once 'vendor/autoload.php';
use Pirsch\Client;
$client = new Client('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET');
// Track a pageview
$client->hit([
'url' => 'https://example.com' . $_SERVER['REQUEST_URI'],
'ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'accept_language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
'referrer' => $_SERVER['HTTP_REFERER'] ?? ''
]);
Creating Access Clients
Server-side SDKs require an access client (not the identification code used by the JavaScript snippet).
- Go to Pirsch dashboard > Settings > Integration
- Under "Access Clients", click "Add Client"
- Select scope: "Write" for sending hits/events, "Read" for querying the API
- Copy the client ID and client secret (the secret is shown only once)
Reverse Proxy Setup
Proxying Pirsch through your own domain gives you ad blocker resistance and real-time dashboard support simultaneously. The visitor's browser loads the script from your domain, so ad blockers that filter by domain (matching pirsch.io) will not block it.
Nginx
# Serve the Pirsch script from your domain
location = /a/p.js {
proxy_pass https://api.pirsch.io/pirsch-extended.js;
proxy_ssl_server_name on;
proxy_set_header Host api.pirsch.io;
expires 1h;
}
# Proxy pageview hits
location = /a/hit {
proxy_pass https://api.pirsch.io/api/v1/hit;
proxy_ssl_server_name on;
proxy_set_header Host api.pirsch.io;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
# Proxy event hits
location = /a/event {
proxy_pass https://api.pirsch.io/api/v1/event;
proxy_ssl_server_name on;
proxy_set_header Host api.pirsch.io;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
}
Caddy
example.com {
handle /a/p.js {
reverse_proxy https://api.pirsch.io {
rewrite /pirsch-extended.js
header_up Host api.pirsch.io
}
}
handle /a/hit {
reverse_proxy https://api.pirsch.io {
rewrite /api/v1/hit
header_up Host api.pirsch.io
header_up X-Forwarded-For {remote_host}
}
}
handle /a/event {
reverse_proxy https://api.pirsch.io {
rewrite /api/v1/event
header_up Host api.pirsch.io
header_up X-Forwarded-For {remote_host}
}
}
}
Cloudflare Workers
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const routes = {
'/a/p.js': 'https://api.pirsch.io/pirsch-extended.js',
'/a/hit': 'https://api.pirsch.io/api/v1/hit',
'/a/event': 'https://api.pirsch.io/api/v1/event'
};
const target = routes[url.pathname];
if (!target) {
return new Response('Not Found', { status: 404 });
}
const headers = new Headers(request.headers);
headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP'));
return fetch(target, {
method: request.method,
headers: headers,
body: request.method === 'POST' ? request.body : undefined
});
}
Updated Script Tag (Proxied)
After setting up the proxy, update your script tag to use your own paths:
<script defer src="/a/p.js"
id="pirschextendedjs"
data-code="YOUR_CODE"
data-hit-endpoint="/a/hit"
data-event-endpoint="/a/event"></script>
The script filename and paths are intentionally non-descriptive (/a/p.js instead of /analytics/pirsch.js) to avoid pattern-based ad blocker rules.
Event Tracking Setup
Loading the Extended Script
Events require pirsch-extended.js. If you loaded the basic pirsch.js, switch to:
<script defer src="https://api.pirsch.io/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"></script>
Tracking Click Events
document.getElementById('cta-button').addEventListener('click', function () {
pirsch('CTA Click', {
meta: {
button: 'hero-signup',
page: window.location.pathname
}
});
});
Tracking Form Submissions
document.getElementById('contact-form').addEventListener('submit', function (e) {
pirsch('Form Submit', {
meta: {
form: 'contact',
page: window.location.pathname
}
});
// Do not call e.preventDefault() unless you handle submission yourself
});
E-Commerce Event Tracking
// Product view
pirsch('Product View', {
meta: {
product_id: 'SKU-1234',
product_name: 'Widget Pro',
price: '49.99',
category: 'widgets'
}
});
// Add to cart
pirsch('Add to Cart', {
meta: {
product_id: 'SKU-1234',
quantity: '1',
cart_total: '49.99'
}
});
// Purchase complete
pirsch('Purchase', {
meta: {
order_id: 'ORD-5678',
total: '49.99',
currency: 'USD',
items: '1'
}
});
All metadata values must be strings. Pirsch does not perform numeric aggregation on metadata -- it stores and filters by exact string match. Use the API to export raw event data if you need to calculate sums or averages.
Self-Hosted Deployment
Docker Compose
version: '3.8'
services:
pirsch:
image: pirsch/server:latest
ports:
- "8080:8080"
environment:
PIRSCH_DB_HOST: postgres
PIRSCH_DB_PORT: 5432
PIRSCH_DB_USER: pirsch
PIRSCH_DB_PASSWORD: ${PIRSCH_DB_PASSWORD:?required}
PIRSCH_DB_NAME: pirsch
PIRSCH_DB_SSL_MODE: disable
PIRSCH_SECRET: ${PIRSCH_SECRET:?required}
PIRSCH_SMTP_HOST: smtp.example.com
PIRSCH_SMTP_PORT: 587
PIRSCH_SMTP_USER: ${PIRSCH_SMTP_USER}
PIRSCH_SMTP_PASSWORD: ${PIRSCH_SMTP_PASSWORD}
depends_on:
- postgres
postgres:
image: postgres:16
volumes:
- pirsch_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: pirsch
POSTGRES_PASSWORD: ${PIRSCH_DB_PASSWORD:?required}
POSTGRES_DB: pirsch
volumes:
pirsch_data:
Running Migrations
On first start, run the database migrations:
docker compose exec pirsch /app/pirsch migrate
Reverse Proxy for Self-Hosted
Put Pirsch behind a reverse proxy with TLS termination. In Caddy:
analytics.example.com {
reverse_proxy pirsch:8080
}
Then configure your script tag to use your self-hosted domain:
<script defer src="https://analytics.example.com/pirsch-extended.js"
id="pirschextendedjs"
data-code="YOUR_CODE"
data-hit-endpoint="https://analytics.example.com/api/v1/hit"
data-event-endpoint="https://analytics.example.com/api/v1/event"></script>
Content Security Policy
If your site uses a CSP, add these directives:
Content-Security-Policy:
script-src 'self' https://api.pirsch.io;
connect-src 'self' https://api.pirsch.io;
For proxied setups, you only need 'self' since all requests go to your own domain.
Troubleshooting Installation
Script Loads But No Data Appears
- Open DevTools > Network > filter
pirsch - Check the POST to
/api/v1/hit-- if it returns 400, the identification code is wrong - If no POST appears, the script loaded but
data-codemay be missing - Check
data-dev-- if your hostname matches an exclusion pattern, tracking is silently disabled
Ad Blocker Blocks Pirsch
Common ad blocker lists (EasyList, uBlock Origin) may block api.pirsch.io. Solutions:
- Set up a reverse proxy (see above)
- Use server-side tracking
- Accept the data gap (typically 15-25% of developer-audience traffic)
Server-Side SDK Returns 403
The access client scope may not include write permissions. Go to Settings > Integration > Access Clients, verify the client has "Write" scope. If it only has "Read", create a new client with write access.
Visitor Count Lower Than Expected
Pirsch's daily fingerprint rotation and lack of cookies mean it undercounts compared to cookie-based tools:
- A visitor who visits in the morning and returns in the evening (same day) counts as one visitor
- A visitor who visits Monday and Tuesday counts as two unique visitors (fingerprint rotated at midnight)
- A visitor on mobile Chrome and desktop Firefox on the same day counts as two visitors (different User-Agent strings)
This is by design. The privacy trade-off produces lower but more honest visitor counts.
Complete Setup Guides
- Install Tracking Code -- Script installation for all platforms
- Event Tracking Setup -- Custom events and conversion tracking
- Data Layer Setup -- Passing metadata with events
- Cross-Domain Tracking -- Multi-domain analytics
- Server-Side vs Client-Side -- Choosing the right tracking method