Zum Hauptinhalt springen
8 Min. Lesedauer

Server-Side Analytics Integration

This guide covers building robust server-side integrations with the Zenovay API. You will learn how to create reusable API wrappers in Node.js and Python, implement caching, batch-fetch data, and set up scheduled pipelines.


Node.js Wrapper Class

A reusable client that handles authentication, error handling, and rate-limit retries:

lib/zenovay-client.tsTypeScript
interface ZenovayConfig {
apiKey: string;
baseUrl?: string;
maxRetries?: number;
}

interface ZenovayResponse<T> {
success: boolean;
data: T;
timestamp: string;
}

export class ZenovayClient {
private apiKey: string;
private baseUrl: string;
private maxRetries: number;

constructor(config: ZenovayConfig) {
  this.apiKey = config.apiKey;
  this.baseUrl = config.baseUrl || 'https://api.zenovay.com/api/external/v1';
  this.maxRetries = config.maxRetries ?? 3;
}

private async request<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
  const url = new URL(`${this.baseUrl}${endpoint}`);
  if (params) {
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  }

  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
    const response = await fetch(url.toString(), {
      headers: { 'X-API-Key': this.apiKey },
    });

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      continue;
    }

    const json: ZenovayResponse<T> = await response.json();

    if (!json.success) {
      throw new Error(`Zenovay API error (${response.status}): ${JSON.stringify(json)}`);
    }

    return json.data;
  }

  throw new Error('Max retries exceeded');
}

async getUsage() {
  return this.request<any>('/usage');
}

async listWebsites() {
  return this.request<{ websites: any[]; total: number }>('/websites');
}

async getAnalytics(websiteId: string, range = '7d') {
  return this.request<any>(`/analytics/${websiteId}`, { range });
}

async getVisitors(websiteId: string, options?: { range?: string; limit?: number; offset?: number }) {
  const params: Record<string, string> = { range: options?.range || '7d' };
  if (options?.limit) params.limit = String(options.limit);
  if (options?.offset) params.offset = String(options.offset);
  return this.request<any>(`/analytics/${websiteId}/visitors`, params);
}

async getPages(websiteId: string, range = '7d', limit = 20) {
  return this.request<any>(`/analytics/${websiteId}/pages`, { range, limit: String(limit) });
}

async getCountries(websiteId: string, range = '7d') {
  return this.request<any>(`/analytics/${websiteId}/countries`, { range });
}

async getTechnology(websiteId: string, range = '7d') {
  return this.request<any>(`/analytics/${websiteId}/technology`, { range });
}

async getErrorGroups(websiteId: string, status?: string) {
  const params: Record<string, string> = {};
  if (status) params.status = status;
  return this.request<any>(`/errors/${websiteId}/groups`, params);
}
}

Usage:

Usage exampleTypeScript
const zenovay = new ZenovayClient({ apiKey: process.env.ZENOVAY_API_KEY! });

const analytics = await zenovay.getAnalytics('ws_abc123', '30d');
console.log('Total visitors:', analytics.summary.total_visitors);

const countries = await zenovay.getCountries('ws_abc123', '30d');
console.log('Top country:', countries.countries[0]?.country_name);

Python Wrapper Class

The same pattern in Python using the requests library:

zenovay_client.pyPython
import time
import requests
from typing import Optional


class ZenovayClient:
  def __init__(self, api_key: str, base_url: str = "https://api.zenovay.com/api/external/v1", max_retries: int = 3):
      self.api_key = api_key
      self.base_url = base_url
      self.max_retries = max_retries

  def _request(self, endpoint: str, params: Optional[dict] = None):
      url = f"{self.base_url}{endpoint}"
      headers = {"X-API-Key": self.api_key}

      for attempt in range(self.max_retries + 1):
          response = requests.get(url, headers=headers, params=params)

          if response.status_code == 429:
              retry_after = int(response.headers.get("Retry-After", 60))
              time.sleep(retry_after)
              continue

          data = response.json()
          if not data.get("success"):
              raise Exception(f"Zenovay API error ({response.status_code}): {data}")

          return data["data"]

      raise Exception("Max retries exceeded")

  def get_usage(self):
      return self._request("/usage")

  def list_websites(self):
      return self._request("/websites")

  def get_analytics(self, website_id: str, range: str = "7d"):
      return self._request(f"/analytics/{website_id}", {"range": range})

  def get_visitors(self, website_id: str, range: str = "7d", limit: int = 100, offset: int = 0):
      return self._request(f"/analytics/{website_id}/visitors", {
          "range": range, "limit": limit, "offset": offset
      })

  def get_pages(self, website_id: str, range: str = "7d", limit: int = 20):
      return self._request(f"/analytics/{website_id}/pages", {"range": range, "limit": limit})

  def get_countries(self, website_id: str, range: str = "7d"):
      return self._request(f"/analytics/{website_id}/countries", {"range": range})

  def get_technology(self, website_id: str, range: str = "7d"):
      return self._request(f"/analytics/{website_id}/technology", {"range": range})

  def get_error_groups(self, website_id: str, status: Optional[str] = None):
      params = {}
      if status:
          params["status"] = status
      return self._request(f"/errors/{website_id}/groups", params)

Usage:

Usage examplePython
import os
from zenovay_client import ZenovayClient

client = ZenovayClient(api_key=os.environ["ZENOVAY_API_KEY"])

analytics = client.get_analytics("ws_abc123", range="30d")
print(f"Total visitors: {analytics['summary']['total_visitors']}")

countries = client.get_countries("ws_abc123", range="30d")
print(f"Top country: {countries['countries'][0]['country_name']}")

Caching Strategies

Analytics data does not change every second. Caching reduces API calls and improves response times.

In-Memory Cache (Node.js)

lib/cache.tsTypeScript
interface CacheEntry<T> {
data: T;
expiresAt: number;
}

const cache = new Map<string, CacheEntry<any>>();

export function getCached<T>(key: string): T | null {
const entry = cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
  cache.delete(key);
  return null;
}
return entry.data;
}

export function setCache<T>(key: string, data: T, ttlSeconds: number): void {
cache.set(key, { data, expiresAt: Date.now() + ttlSeconds * 1000 });
}

// Usage with Zenovay client
export async function getCachedAnalytics(client: ZenovayClient, websiteId: string, range: string) {
const cacheKey = `analytics:${websiteId}:${range}`;
const cached = getCached(cacheKey);
if (cached) return cached;

const data = await client.getAnalytics(websiteId, range);
setCache(cacheKey, data, 300); // Cache for 5 minutes
return data;
}

Redis Cache

lib/redis-cache.tsTypeScript
import { createClient } from 'redis';

const redis = createClient({ url: process.env.REDIS_URL });
redis.connect();

export async function getCachedOrFetch<T>(
key: string,
ttlSeconds: number,
fetcher: () => Promise<T>
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);

const data = await fetcher();
await redis.setEx(key, ttlSeconds, JSON.stringify(data));
return data;
}

// Usage
const analytics = await getCachedOrFetch(
`zenovay:analytics:ws_abc123:30d`,
300,
() => client.getAnalytics('ws_abc123', '30d')
);
Data TypeTTLReason
Analytics overview5 minutesAggregated data, updates infrequently
Visitor list2 minutesNew visitors arrive regularly
Countries10 minutesGeographic distribution is stable
Technology10 minutesDevice/browser mix changes slowly
Live visitorsNo cacheReal-time data, always fetch fresh

Batch Data Fetching

When building dashboards that need multiple data points, fetch in parallel:

Parallel fetchingTypeScript
async function fetchDashboardData(client: ZenovayClient, websiteId: string, range: string) {
const [analytics, countries, technology, pages] = await Promise.all([
  client.getAnalytics(websiteId, range),
  client.getCountries(websiteId, range),
  client.getTechnology(websiteId, range),
  client.getPages(websiteId, range),
]);

return { analytics, countries, technology, pages };
}

For fetching data across multiple websites:

Multi-website fetchingTypeScript
async function fetchAllWebsiteStats(client: ZenovayClient, range: string) {
const { websites } = await client.listWebsites();

const results = await Promise.all(
  websites.map(async (website: any) => {
    const analytics = await client.getAnalytics(website.id, range);
    return {
      website: { id: website.id, domain: website.domain, name: website.name },
      summary: analytics.summary,
    };
  })
);

return results;
}

Scheduled Data Pipelines

Use cron jobs to collect analytics data on a regular schedule.

Node.js with node-cron

scripts/daily-export.tsTypeScript
import cron from 'node-cron';
import { ZenovayClient } from '../lib/zenovay-client';
import fs from 'fs/promises';

const client = new ZenovayClient({ apiKey: process.env.ZENOVAY_API_KEY! });

// Run daily at 2:00 AM
cron.schedule('0 2 * * *', async () => {
console.log('Starting daily analytics export...');

try {
  const { websites } = await client.listWebsites();

  for (const website of websites) {
    const analytics = await client.getAnalytics(website.id, '24h');
    const countries = await client.getCountries(website.id, '24h');

    const date = new Date().toISOString().split('T')[0];
    const filename = `exports/${website.domain}_${date}.json`;

    await fs.mkdir('exports', { recursive: true });
    await fs.writeFile(filename, JSON.stringify({
      website: website.domain,
      date,
      analytics: analytics.summary,
      countries: countries.countries,
    }, null, 2));

    console.log(`Exported: ${filename}`);
  }
} catch (error) {
  console.error('Export failed:', error);
}
});

console.log('Daily export cron job scheduled.');

Python with Schedule

scripts/daily_export.pyPython
import os
import json
import schedule
import time
from datetime import date
from zenovay_client import ZenovayClient

client = ZenovayClient(api_key=os.environ["ZENOVAY_API_KEY"])


def daily_export():
  print("Starting daily analytics export...")

  try:
      websites = client.list_websites()["websites"]

      for website in websites:
          analytics = client.get_analytics(website["id"], range="24h")
          countries = client.get_countries(website["id"], range="24h")

          today = date.today().isoformat()
          filename = f"exports/{website['domain']}_{today}.json"

          os.makedirs("exports", exist_ok=True)
          with open(filename, "w") as f:
              json.dump({
                  "website": website["domain"],
                  "date": today,
                  "analytics": analytics["summary"],
                  "countries": countries["countries"],
              }, f, indent=2)

          print(f"Exported: {filename}")

  except Exception as e:
      print(f"Export failed: {e}")


schedule.every().day.at("02:00").do(daily_export)

print("Daily export cron job scheduled.")
while True:
  schedule.run_pending()
  time.sleep(60)

Monitoring and Alerting

Build alerts that fire when analytics metrics cross thresholds:

scripts/traffic-alert.tsTypeScript
import { ZenovayClient } from '../lib/zenovay-client';

const client = new ZenovayClient({ apiKey: process.env.ZENOVAY_API_KEY! });

interface AlertRule {
websiteId: string;
metric: 'total_visitors' | 'total_page_views';
threshold: number;
direction: 'above' | 'below';
}

const rules: AlertRule[] = [
{ websiteId: 'ws_abc123', metric: 'total_visitors', threshold: 100, direction: 'below' },
{ websiteId: 'ws_abc123', metric: 'total_page_views', threshold: 50000, direction: 'above' },
];

async function checkAlerts() {
for (const rule of rules) {
  const analytics = await client.getAnalytics(rule.websiteId, '24h');
  const value = analytics.summary[rule.metric];

  const triggered =
    rule.direction === 'above' ? value > rule.threshold : value < rule.threshold;

  if (triggered) {
    console.log(
      `ALERT: ${rule.metric} is ${value} (${rule.direction} threshold of ${rule.threshold})`
    );
    // Send notification via email, Slack, PagerDuty, etc.
  }
}
}

// Run every hour
setInterval(checkAlerts, 60 * 60 * 1000);
checkAlerts();

Next Steps

War diese Seite hilfreich?