Pirsch Setup: Script Install, Server-Side SDK, | OpsBlu Docs

Pirsch Setup: Script Install, Server-Side SDK,

Step-by-step Pirsch analytics installation covering the JavaScript snippet, Go and Node.js server-side SDKs, SPA integration, reverse proxy setup, and...

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:

  1. A GET request for pirsch.js or pirsch-extended.js (the script itself)
  2. 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:

  1. Go to Online Store > Themes > Edit Code
  2. Open layout/theme.liquid
  3. 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).

  1. Go to Pirsch dashboard > Settings > Integration
  2. Under "Access Clients", click "Add Client"
  3. Select scope: "Write" for sending hits/events, "Read" for querying the API
  4. 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

  1. Open DevTools > Network > filter pirsch
  2. Check the POST to /api/v1/hit -- if it returns 400, the identification code is wrong
  3. If no POST appears, the script loaded but data-code may be missing
  4. 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:

  1. Set up a reverse proxy (see above)
  2. Use server-side tracking
  3. 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