Skip to main content
11 min read

Custom Frameworks

Integrate Zenovay analytics with custom frameworks, build tools, and unique architectures. This guide covers patterns for any setup not covered by our official integrations.


Quick Reference

Integration TypeComplexityBest For
Script TagSimpleStatic sites, basic setups
JavaScript APIMediumSPAs, dynamic routing
HTTP APIAdvancedServer-side, headless, IoT
First-Party ProxyAdvancedAd-blocker bypass

Core Integration Methods

Method 1: Script Tag (Simplest)

Add the tracking script to any HTML page:

<!DOCTYPE html>
<html>
<head>
    <title>Your Site</title>
    <!-- Zenovay Analytics -->
    <script defer data-tracking-code="YOUR_TRACKING_CODE" src="https://api.zenovay.com/z.js"></script>
</head>
<body>
    <!-- Your content -->
</body>
</html>

Attributes:

AttributeRequiredDescription
data-tracking-codeYesYour tracking code
deferRecommendedNon-blocking load
data-api-urlNoFirst-party proxy URL
data-autoNoSet false to disable auto page views

Method 2: JavaScript API

For programmatic control:

// Queue function (works before script loads)
window.zenovay = window.zenovay || function() {
    (window.zenovay.q = window.zenovay.q || []).push(arguments);
};

// Track page views manually
zenovay('page');

// Track custom events
zenovay('track', 'button_click', {
    button_name: 'signup',
    location: 'header'
});

// Identify users
zenovay('identify', 'user_123', {
    email: '[email protected]',
    plan: 'pro'
});

Method 3: HTTP API

For server-side or non-browser environments:

// Node.js example
const response = await fetch('https://api.zenovay.com/e/YOUR_TRACKING_CODE', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-Forwarded-For': clientIp // Important for geo
    },
    body: JSON.stringify({
        event: 'pageview',
        url: 'https://yoursite.com/page',
        referrer: 'https://google.com',
        user_agent: clientUserAgent,
        timestamp: new Date().toISOString()
    })
});

See API Endpoints for full API documentation.


Single Page Applications (SPAs)

SPAs require manual page view tracking on route changes.

Generic SPA Pattern

// Create a tracking wrapper
class ZenovayTracker {
    constructor(siteId) {
        this.siteId = siteId;
        this.previousPath = null;
    }

    init() {
        // Queue function (works before script loads)
        window.zenovay = window.zenovay || function() {
            (window.zenovay.q = window.zenovay.q || []).push(arguments);
        };
    }

    trackPageView(path) {
        // Prevent duplicate tracking
        if (path === this.previousPath) return;
        this.previousPath = path;

        zenovay('page');
    }

    trackEvent(name, properties = {}) {
        zenovay('track', name, properties);
    }
}

// Usage
const tracker = new ZenovayTracker('YOUR_TRACKING_CODE');
tracker.init();

// On route change
router.on('change', (route) => {
    tracker.trackPageView(route.path);
});

History API Integration

For SPAs using History API:

// Intercept pushState and replaceState
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;

history.pushState = function(...args) {
    originalPushState.apply(this, args);
    window.dispatchEvent(new Event('locationchange'));
};

history.replaceState = function(...args) {
    originalReplaceState.apply(this, args);
    window.dispatchEvent(new Event('locationchange'));
};

// Also handle popstate (back/forward)
window.addEventListener('popstate', () => {
    window.dispatchEvent(new Event('locationchange'));
});

// Track on location change
window.addEventListener('locationchange', () => {
    zenovay('page');
});

Hash-Based Routing

For apps using hash routing:

window.addEventListener('hashchange', () => {
    zenovay('page');
});

// Initial page view
document.addEventListener('DOMContentLoaded', () => {
    zenovay('page');
});

Static Site Generators

Eleventy (11ty)

Create _includes/analytics.njk:

<!-- Zenovay Analytics -->
<script defer data-tracking-code="{{ site.zenovayId }}" src="https://api.zenovay.com/z.js"></script>

Add to your base template:

<!DOCTYPE html>
<html>
<head>
    {% include "analytics.njk" %}
</head>
<body>
    {{ content | safe }}
</body>
</html>

In _data/site.js:

module.exports = {
    zenovayId: process.env.ZENOVAY_TRACKING_CODE || 'YOUR_TRACKING_CODE'
};

Hugo

Create layouts/partials/analytics.html:

{{ if not .Site.IsServer }}
<script defer data-tracking-code="{{ .Site.Params.zenovayId }}" src="https://api.zenovay.com/z.js"></script>
{{ end }}

In config.toml:

[params]
zenovayId = "YOUR_TRACKING_CODE"

Include in layouts/_default/baseof.html:

<head>
    {{ partial "analytics.html" . }}
</head>

Jekyll

Create _includes/analytics.html:

{% if jekyll.environment == "production" %}
<script defer data-tracking-code="{{ site.zenovay_id }}" src="https://api.zenovay.com/z.js"></script>
{% endif %}

In _config.yml:

zenovay_id: "YOUR_TRACKING_CODE"

Gatsby

Create gatsby-ssr.js:

import React from "react";

export const onRenderBody = ({ setHeadComponents }) => {
    if (process.env.NODE_ENV === "production") {
        setHeadComponents([
            <script
                key="zenovay"
                defer
                data-tracking-code={process.env.GATSBY_ZENOVAY_ID}
                src="https://api.zenovay.com/z.js"
            />
        ]);
    }
};

For route changes, in gatsby-browser.js:

export const onRouteUpdate = ({ location }) => {
    if (typeof window.zenovay !== 'undefined') {
        window.zenovay('page');
    }
};

Remix

Add to app/root.tsx:

import { Scripts } from "@remix-run/react";

export default function App() {
    return (
        <html>
            <head>
                {process.env.NODE_ENV === "production" && (
                    <script
                        defer
                        data-tracking-code={process.env.ZENOVAY_ID}
                        src="https://api.zenovay.com/z.js"
                    />
                )}
            </head>
            <body>
                <Outlet />
                <Scripts />
            </body>
        </html>
    );
}

Progressive Web Apps (PWAs)

Installation Tracking

// Track PWA installation
window.addEventListener('beforeinstallprompt', (e) => {
    zenovay('track', 'pwa_install_prompt_shown');
});

window.addEventListener('appinstalled', () => {
    zenovay('track', 'pwa_installed', {
        timestamp: new Date().toISOString()
    });
});

Offline Tracking Queue

Track events while offline, send when online:

class OfflineQueue {
    constructor() {
        this.storageKey = 'zenovay_offline_queue';
        this.init();
    }

    init() {
        // Send queued events when coming online
        window.addEventListener('online', () => this.flush());

        // Check if we need to flush on load
        if (navigator.onLine) {
            this.flush();
        }
    }

    getQueue() {
        try {
            return JSON.parse(localStorage.getItem(this.storageKey)) || [];
        } catch {
            return [];
        }
    }

    saveQueue(queue) {
        localStorage.setItem(this.storageKey, JSON.stringify(queue));
    }

    add(event, properties) {
        const queue = this.getQueue();
        queue.push({
            event,
            properties,
            timestamp: new Date().toISOString(),
            url: window.location.href
        });
        this.saveQueue(queue);

        if (navigator.onLine) {
            this.flush();
        }
    }

    async flush() {
        const queue = this.getQueue();
        if (queue.length === 0) return;

        const successful = [];

        for (const item of queue) {
            try {
                zenovay('track', item.event, {
                    ...item.properties,
                    _queued_at: item.timestamp,
                    _sent_at: new Date().toISOString()
                });
                successful.push(item);
            } catch (error) {
                console.error('Failed to send queued event:', error);
                break; // Stop on first failure
            }
        }

        // Remove successfully sent items
        const remaining = queue.filter(item => !successful.includes(item));
        this.saveQueue(remaining);
    }
}

// Usage
const offlineQueue = new OfflineQueue();

// Track event (works offline)
offlineQueue.add('page_view', { path: '/home' });

Service Worker Integration

In your service worker:

// sw.js
self.addEventListener('sync', (event) => {
    if (event.tag === 'zenovay-sync') {
        event.waitUntil(syncAnalytics());
    }
});

async function syncAnalytics() {
    const cache = await caches.open('zenovay-analytics');
    const requests = await cache.keys();

    for (const request of requests) {
        try {
            await fetch(request);
            await cache.delete(request);
        } catch {
            // Will retry on next sync
        }
    }
}

Web Components

Custom Element with Tracking

class TrackableButton extends HTMLElement {
    static get observedAttributes() {
        return ['track-event', 'track-properties'];
    }

    connectedCallback() {
        this.addEventListener('click', this.handleClick.bind(this));
    }

    handleClick() {
        const eventName = this.getAttribute('track-event') || 'button_click';
        let properties = {};

        try {
            properties = JSON.parse(this.getAttribute('track-properties') || '{}');
        } catch {}

        if (window.zenovay) {
            zenovay('track', eventName, {
                ...properties,
                element: this.tagName.toLowerCase(),
                text: this.textContent
            });
        }
    }
}

customElements.define('trackable-button', TrackableButton);

Usage:

<trackable-button
    track-event="cta_click"
    track-properties='{"location": "hero", "variant": "primary"}'>
    Get Started
</trackable-button>

Shadow DOM Considerations

When using Shadow DOM, attach event listeners to the shadow root:

class TrackedComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
        this.render();
        this.setupTracking();
    }

    render() {
        // Create button element safely
        const button = document.createElement('button');
        button.id = 'cta';
        button.textContent = 'Click Me';
        this.shadowRoot.appendChild(button);
    }

    setupTracking() {
        // Track clicks within shadow DOM
        this.shadowRoot.addEventListener('click', (e) => {
            if (e.target.id === 'cta') {
                zenovay('track', 'shadow_button_click', {
                    component: 'tracked-component'
                });
            }
        });

        // Track visibility with Intersection Observer
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    zenovay('track', 'component_viewed', {
                        component: 'tracked-component'
                    });
                    observer.disconnect();
                }
            });
        });

        observer.observe(this);
    }
}

Build Tool Integrations

Vite Plugin

Create vite-plugin-zenovay.js:

export default function zenovayPlugin(options = {}) {
    const { siteId, enabled = true } = options;

    return {
        name: 'vite-plugin-zenovay',
        transformIndexHtml(html) {
            if (!enabled || !siteId) return html;

            const script = `<script defer data-tracking-code="${siteId}" src="https://api.zenovay.com/z.js"></script>`;

            return html.replace('</head>', `${script}</head>`);
        }
    };
}

// Usage in vite.config.js
import zenovayPlugin from './vite-plugin-zenovay';

export default {
    plugins: [
        zenovayPlugin({
            siteId: process.env.VITE_ZENOVAY_ID,
            enabled: process.env.NODE_ENV === 'production'
        })
    ]
};

Webpack Plugin

Create webpack-zenovay-plugin.js:

const HtmlWebpackPlugin = require('html-webpack-plugin');

class ZenovayWebpackPlugin {
    constructor(options = {}) {
        this.options = options;
    }

    apply(compiler) {
        compiler.hooks.compilation.tap('ZenovayPlugin', (compilation) => {
            HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
                'ZenovayPlugin',
                (data, cb) => {
                    if (this.options.siteId && this.options.enabled !== false) {
                        const script = `<script defer data-tracking-code="${this.options.siteId}" src="https://api.zenovay.com/z.js"></script>`;
                        data.html = data.html.replace('</head>', `${script}</head>`);
                    }
                    cb(null, data);
                }
            );
        });
    }
}

module.exports = ZenovayWebpackPlugin;

// Usage in webpack.config.js
const ZenovayPlugin = require('./webpack-zenovay-plugin');

module.exports = {
    plugins: [
        new HtmlWebpackPlugin(),
        new ZenovayPlugin({
            siteId: process.env.ZENOVAY_ID,
            enabled: process.env.NODE_ENV === 'production'
        })
    ]
};

Rollup Plugin

// rollup-plugin-zenovay.js
export default function zenovayPlugin(options = {}) {
    return {
        name: 'zenovay',
        generateBundle(outputOptions, bundle) {
            for (const fileName in bundle) {
                if (fileName.endsWith('.html')) {
                    const chunk = bundle[fileName];
                    if (chunk.type === 'asset') {
                        const script = `<script defer data-tracking-code="${options.siteId}" src="https://api.zenovay.com/z.js"></script>`;
                        chunk.source = chunk.source.replace('</head>', `${script}</head>`);
                    }
                }
            }
        }
    };
}

Server-Side Tracking

Node.js SDK Pattern

// zenovay-server.js
class ZenovayServer {
    constructor(siteId, options = {}) {
        this.siteId = siteId;
        this.apiUrl = options.apiUrl || 'https://api.zenovay.com/e/';
        this.queue = [];
        this.flushInterval = options.flushInterval || 5000;
        this.batchSize = options.batchSize || 10;

        this.startFlushTimer();
    }

    startFlushTimer() {
        setInterval(() => this.flush(), this.flushInterval);
    }

    track(event, properties = {}, context = {}) {
        this.queue.push({
            event,
            properties,
            user_agent: context.userAgent,
            ip: context.ip,
            url: context.url,
            referrer: context.referrer,
            timestamp: new Date().toISOString()
        });

        if (this.queue.length >= this.batchSize) {
            this.flush();
        }
    }

    async flush() {
        if (this.queue.length === 0) return;

        const events = this.queue.splice(0, this.batchSize);

        try {
            await fetch(this.apiUrl + this.siteId, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ events })
            });
        } catch (error) {
            // Re-queue on failure
            this.queue.unshift(...events);
            console.error('Zenovay: Failed to send events', error);
        }
    }

    // Express middleware
    middleware() {
        return (req, res, next) => {
            req.trackEvent = (event, properties) => {
                this.track(event, properties, {
                    userAgent: req.headers['user-agent'],
                    ip: req.ip,
                    url: req.originalUrl,
                    referrer: req.headers.referer
                });
            };
            next();
        };
    }
}

module.exports = ZenovayServer;

// Usage
const ZenovayServer = require('./zenovay-server');
const zenovay = new ZenovayServer('YOUR_TRACKING_CODE');

app.use(zenovay.middleware());

app.get('/checkout', (req, res) => {
    req.trackEvent('checkout_started', {
        cart_value: 99.99
    });
    res.render('checkout');
});

Python Example

import requests
from datetime import datetime
from threading import Thread
from queue import Queue

class ZenovayTracker:
    def __init__(self, tracking_code, api_url='https://api.zenovay.com/e/'):
        self.tracking_code = tracking_code
        self.api_url = api_url
        self.queue = Queue()
        self._start_worker()

    def _start_worker(self):
        def worker():
            while True:
                event = self.queue.get()
                try:
                    requests.post(self.api_url + self.tracking_code, json=event, timeout=5)
                except Exception as e:
                    print(f"Zenovay error: {e}")
                self.queue.task_done()

        thread = Thread(target=worker, daemon=True)
        thread.start()

    def track(self, event, properties=None, context=None):
        context = context or {}
        self.queue.put({
            'event': event,
            'properties': properties or {},
            'user_agent': context.get('user_agent'),
            'ip': context.get('ip'),
            'url': context.get('url'),
            'timestamp': datetime.utcnow().isoformat()
        })

# Flask example
from flask import Flask, request

app = Flask(__name__)
tracker = ZenovayTracker('YOUR_TRACKING_CODE')

@app.route('/purchase', methods=['POST'])
def purchase():
    tracker.track('purchase', {
        'revenue': request.json.get('total'),
        'currency': 'USD'
    }, {
        'user_agent': request.headers.get('User-Agent'),
        'ip': request.remote_addr,
        'url': request.url
    })
    return {'status': 'ok'}

Testing & Debugging

Debug Mode

// Enable debug mode
zenovay('debug');

// All events will be logged to console
zenovay('track', 'test_event', { test: true });
// Console: [Zenovay] Event tracked: test_event { test: true }

Mock Tracker for Tests

// __mocks__/zenovay.js
const mockZenovay = {
    calls: [],

    handler(...args) {
        mockZenovay.calls.push(args);
    },

    reset() {
        mockZenovay.calls = [];
    },

    getEvents() {
        return mockZenovay.calls.filter(c => c[0] === 'track');
    }
};

window.zenovay = (...args) => mockZenovay.handler(...args);
window.zenovay.mock = mockZenovay;

// In your test
test('tracks button click', () => {
    window.zenovay.mock.reset();

    fireEvent.click(getByRole('button', { name: 'Sign Up' }));

    expect(window.zenovay.mock.getEvents()).toContainEqual(
        ['track', 'signup_click', { location: 'header' }]
    );
});

Network Inspection

Check tracking is working:

  1. Open DevTools → Network tab
  2. Filter by zenovay or z.js
  3. Click around your site
  4. Verify requests to api.zenovay.com

Content Security Policy

Configure CSP headers to allow Zenovay:

Content-Security-Policy:
    default-src 'self';
    script-src 'self' https://api.zenovay.com;
    connect-src 'self' https://api.zenovay.com;

For first-party tracking (recommended):

Content-Security-Policy:
    default-src 'self';
    script-src 'self';
    connect-src 'self';

Best Practices

  1. Load Asynchronously: Always use defer or async to prevent blocking
  2. Handle Errors: Wrap tracking calls in try-catch for resilience
  3. Respect Privacy: Check consent before tracking, honor DNT
  4. Batch Events: Combine events when possible to reduce requests
  5. Test Thoroughly: Verify tracking in multiple browsers and devices
  6. Use First-Party: Set up first-party tracking for better accuracy
  7. Monitor Performance: Ensure tracking doesn't impact Core Web Vitals


Need help with your integration? Contact [email protected].

Was this page helpful?