メインコンテンツへスキップ
8分で読めます

Webhook & Real-Time Data

This guide covers strategies for getting real-time and near-real-time analytics data from Zenovay, including polling patterns, the public live endpoint, Server-Sent Events, and automated alerting.

Zenovay does not currently offer traditional webhook callbacks for analytics events. Instead, use the polling strategies and public live endpoints described below to achieve real-time data access.


Real-Time Live Visitor Count

The live visitor endpoint is public -- it requires no API key and can be called directly from the browser:

GET https://api.zenovay.com/e/live/YOUR_TRACKING_CODE
Fetch live visitorsBash
curl 'https://api.zenovay.com/e/live/ZV_XXXXXXXXXXX'
ResponseJSON
{
"visitors": 42
}

This endpoint returns the number of visitors currently active on your site (within the last 5 minutes). Poll it every 10 seconds for a smooth real-time counter.


Polling Strategies

Simple Interval Polling

The most straightforward approach -- poll at a fixed interval:

Simple pollingTypeScript
class ZenovayPoller {
private intervalId: ReturnType<typeof setInterval> | null = null;

constructor(
  private apiKey: string,
  private websiteId: string,
  private onData: (data: any) => void,
  private intervalMs: number = 30_000
) {}

start() {
  this.fetchData();
  this.intervalId = setInterval(() => this.fetchData(), this.intervalMs);
}

stop() {
  if (this.intervalId) clearInterval(this.intervalId);
}

private async fetchData() {
  try {
    const res = await fetch(
      `https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}?range=24h`,
      { headers: { 'X-API-Key': this.apiKey } }
    );
    const json = await res.json();
    if (json.success) this.onData(json.data);
  } catch (error) {
    console.error('Polling error:', error);
  }
}
}

// Usage
const poller = new ZenovayPoller(
process.env.ZENOVAY_API_KEY!,
'ws_abc123',
(data) => console.log('Visitors:', data.summary.total_visitors),
30_000 // Poll every 30 seconds
);
poller.start();

Adaptive Polling

Reduce polling frequency when data is not changing, increase it during active periods:

Adaptive pollingTypeScript
class AdaptivePoller {
private intervalId: ReturnType<typeof setInterval> | null = null;
private currentInterval: number;
private lastValue: number | null = null;

private readonly minInterval = 10_000;   // 10 seconds minimum
private readonly maxInterval = 120_000;  // 2 minutes maximum

constructor(
  private fetcher: () => Promise<number>,
  private onUpdate: (value: number) => void,
  initialInterval = 30_000
) {
  this.currentInterval = initialInterval;
}

start() {
  this.tick();
}

stop() {
  if (this.intervalId) clearTimeout(this.intervalId);
}

private async tick() {
  try {
    const value = await this.fetcher();

    if (this.lastValue !== null && value !== this.lastValue) {
      // Data changed -- poll faster
      this.currentInterval = Math.max(this.minInterval, this.currentInterval / 2);
    } else {
      // Data stable -- slow down
      this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 1.5);
    }

    this.lastValue = value;
    this.onUpdate(value);
  } catch {
    // On error, slow down
    this.currentInterval = Math.min(this.maxInterval, this.currentInterval * 2);
  }

  this.intervalId = setTimeout(() => this.tick(), this.currentInterval);
}
}
Data TypeMinimum IntervalRecommendedNotes
Live visitors (public)5s10sNo auth, low cost
Analytics overview30s60sAggregated data
Visitor list30s60sNew records appear regularly
Countries/Technology60s300sChanges slowly
Error groups60s300sUnless actively debugging

Building a Live Dashboard with Polling

Combine the public live endpoint with authenticated API calls for a complete real-time dashboard:

Real-time dashboard serviceTypeScript
interface DashboardState {
liveVisitors: number;
todayVisitors: number;
todayPageViews: number;
topPages: Array<{ url: string; views: number }>;
recentErrors: Array<{ message: string; type: string; occurrence_count: number }>;
}

class LiveDashboard {
private state: DashboardState = {
  liveVisitors: 0,
  todayVisitors: 0,
  todayPageViews: 0,
  topPages: [],
  recentErrors: [],
};
private listeners: Set<(state: DashboardState) => void> = new Set();

constructor(
  private apiKey: string,
  private websiteId: string,
  private trackingCode: string
) {}

subscribe(listener: (state: DashboardState) => void) {
  this.listeners.add(listener);
  listener(this.state); // Send current state immediately
  return () => this.listeners.delete(listener);
}

private notify() {
  this.listeners.forEach(fn => fn({ ...this.state }));
}

start() {
  // Live visitors: every 10 seconds (public, no auth)
  this.pollLive();
  setInterval(() => this.pollLive(), 10_000);

  // Analytics overview: every 30 seconds
  this.pollAnalytics();
  setInterval(() => this.pollAnalytics(), 30_000);

  // Pages and errors: every 60 seconds
  this.pollPages();
  setInterval(() => this.pollPages(), 60_000);
  this.pollErrors();
  setInterval(() => this.pollErrors(), 60_000);
}

private async pollLive() {
  try {
    const res = await fetch(`https://api.zenovay.com/e/live/${this.trackingCode}`);
    const data = await res.json();
    this.state.liveVisitors = data.visitors ?? 0;
    this.notify();
  } catch {}
}

private async pollAnalytics() {
  try {
    const res = await fetch(
      `https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}?range=24h`,
      { headers: { 'X-API-Key': this.apiKey } }
    );
    const json = await res.json();
    if (json.success) {
      this.state.todayVisitors = json.data.summary.total_visitors;
      this.state.todayPageViews = json.data.summary.total_page_views;
      this.notify();
    }
  } catch {}
}

private async pollPages() {
  try {
    const res = await fetch(
      `https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}/pages?range=24h&limit=10`,
      { headers: { 'X-API-Key': this.apiKey } }
    );
    const json = await res.json();
    if (json.success) {
      this.state.topPages = json.data.pages;
      this.notify();
    }
  } catch {}
}

private async pollErrors() {
  try {
    const res = await fetch(
      `https://api.zenovay.com/api/external/v1/errors/${this.websiteId}/groups?status=open`,
      { headers: { 'X-API-Key': this.apiKey } }
    );
    const json = await res.json();
    if (json.success) {
      this.state.recentErrors = json.data.error_groups.slice(0, 5);
      this.notify();
    }
  } catch {}
}
}

Server-Sent Events (SSE) Pattern

Use a backend SSE endpoint to push live data to your frontend without the browser polling Zenovay directly:

SSE endpoint (Express.js)TypeScript
import express from 'express';

const app = express();

app.get('/api/sse/live', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();

const trackingCode = req.query.trackingCode as string;
const apiKey = process.env.ZENOVAY_API_KEY!;
const websiteId = req.query.websiteId as string;

// Poll live visitors every 10 seconds
const liveInterval = setInterval(async () => {
  try {
    const res2 = await fetch(`https://api.zenovay.com/e/live/${trackingCode}`);
    const data = await res2.json();
    res.write(`event: live\ndata: ${JSON.stringify({ visitors: data.visitors })}\n\n`);
  } catch {}
}, 10_000);

// Poll analytics every 30 seconds
const analyticsInterval = setInterval(async () => {
  try {
    const res2 = await fetch(
      `https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=24h`,
      { headers: { 'X-API-Key': apiKey } }
    );
    const json = await res2.json();
    if (json.success) {
      res.write(`event: analytics\ndata: ${JSON.stringify(json.data.summary)}\n\n`);
    }
  } catch {}
}, 30_000);

req.on('close', () => {
  clearInterval(liveInterval);
  clearInterval(analyticsInterval);
});
});

app.listen(3000);

Connect from the browser:

Browser SSE clientTypeScript
const eventSource = new EventSource(
'/api/sse/live?trackingCode=ZV_XXXXXXXXXXX&websiteId=ws_abc123'
);

eventSource.addEventListener('live', (event) => {
const data = JSON.parse(event.data);
document.getElementById('live-count')!.textContent = data.visitors;
});

eventSource.addEventListener('analytics', (event) => {
const summary = JSON.parse(event.data);
document.getElementById('today-visitors')!.textContent =
  summary.total_visitors.toLocaleString();
document.getElementById('today-pageviews')!.textContent =
  summary.total_page_views.toLocaleString();
});

eventSource.onerror = () => {
console.log('SSE connection lost, reconnecting...');
// EventSource auto-reconnects
};

High-Value Visitor Alerting

Monitor the visitor stream and alert when high-value visitors arrive. This is useful for sales teams who want to know when important prospects are browsing:

High-value visitor monitorTypeScript
interface VisitorAlert {
visitorId: string;
country: string;
valueScore: number;
landingPage: string;
timestamp: string;
}

class HighValueVisitorMonitor {
private lastChecked: string | null = null;

constructor(
  private apiKey: string,
  private websiteId: string,
  private valueThreshold: number = 70,
  private onAlert: (alert: VisitorAlert) => void
) {}

async check() {
  try {
    const res = await fetch(
      `https://api.zenovay.com/api/external/v1/analytics/${this.websiteId}/visitors?range=24h&limit=50`,
      { headers: { 'X-API-Key': this.apiKey } }
    );
    const json = await res.json();
    if (!json.success) return;

    const visitors = json.data.visitors;

    for (const visitor of visitors) {
      // Skip if we already processed this visitor
      if (this.lastChecked && visitor.visited_at <= this.lastChecked) continue;

      if (visitor.value_score >= this.valueThreshold) {
        this.onAlert({
          visitorId: visitor.id,
          country: visitor.country_name || visitor.country_code || 'Unknown',
          valueScore: visitor.value_score,
          landingPage: visitor.landing_page || '/',
          timestamp: visitor.visited_at,
        });
      }
    }

    if (visitors.length > 0) {
      this.lastChecked = visitors[0].visited_at;
    }
  } catch (error) {
    console.error('Monitor error:', error);
  }
}

start(intervalMs = 60_000) {
  this.check();
  setInterval(() => this.check(), intervalMs);
}
}

// Usage: alert to Slack when a high-value visitor arrives
const monitor = new HighValueVisitorMonitor(
process.env.ZENOVAY_API_KEY!,
'ws_abc123',
70,
async (alert) => {
  console.log(`High-value visitor! Score: ${alert.valueScore}, Country: ${alert.country}`);

  // Send to Slack
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `High-value visitor (score: ${alert.valueScore}) from ${alert.country} landed on ${alert.landingPage}`,
    }),
  });
}
);

monitor.start(60_000); // Check every minute

Traffic Spike Detection

Detect unusual traffic spikes by comparing current data to recent baselines:

Spike detectionTypeScript
async function detectTrafficSpike(
apiKey: string,
websiteId: string,
thresholdMultiplier = 2.0
): Promise<{ spikeDetected: boolean; currentVisitors: number; baseline: number }> {
// Get today's data
const todayRes = await fetch(
  `https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=24h`,
  { headers: { 'X-API-Key': apiKey } }
);
const todayJson = await todayRes.json();
const currentVisitors = todayJson.data?.summary?.total_visitors || 0;

// Get last 7 days for baseline
const weekRes = await fetch(
  `https://api.zenovay.com/api/external/v1/analytics/${websiteId}?range=7d`,
  { headers: { 'X-API-Key': apiKey } }
);
const weekJson = await weekRes.json();
const dailyStats = weekJson.data?.daily_stats || [];

// Calculate average daily visitors (excluding today)
const pastDays = dailyStats.slice(0, -1);
const avgVisitors = pastDays.length > 0
  ? pastDays.reduce((sum: number, d: any) => sum + d.total_visitors, 0) / pastDays.length
  : 0;

return {
  spikeDetected: avgVisitors > 0 && currentVisitors > avgVisitors * thresholdMultiplier,
  currentVisitors,
  baseline: Math.round(avgVisitors),
};
}

Next Steps

このページは役に立ちましたか?