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

カスタム分析ダッシュボードの構築

このガイドでは、Zenovayデータを表示するカスタム分析ダッシュボードを構築する方法を説明します。推奨アーキテクチャ、APIコールを安全にプロキシする方法、およびチャート、マップ、リアルタイム訪問者カウンターを構築する方法を学習します。


前提条件

APIコールを実行する前に、Zenovay APIキーが必要です。Settings → Security → API keysに移動してAPIキーを作成してください。APIアクセスには有料プランが必要です。

Settings → Security → API keys ページ(個人トークン作成を表示)
Settings → Security → API keys からAPIキーを作成してください。

アーキテクチャの概要

推奨パターンはサーバー側プロキシを使用してAPIキーをセキュアに保つ方法です:

Browser  -->  Your Backend  -->  Zenovay API
                (proxy)          api.zenovay.com

ZenovayのAPIキーをクライアント側JavaScriptに絶対に公開しないでください。常に独自のサーバーを通じてリクエストをプロキシしてください。


サーバー側プロキシの設定

app/api/analytics/route.tsTypeScript
import { NextResponse } from 'next/server';

const ZENOVAY_API_KEY = process.env.ZENOVAY_API_KEY!;
const ZENOVAY_BASE_URL = 'https://api.zenovay.com/api/external/v1';

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const websiteId = searchParams.get('websiteId');
const endpoint = searchParams.get('endpoint') || '';
const range = searchParams.get('range') || '7d';

if (!websiteId) {
  return NextResponse.json({ error: 'websiteId required' }, { status: 400 });
}

const url = `${ZENOVAY_BASE_URL}/analytics/${websiteId}${endpoint ? '/' + endpoint : ''}?range=${range}`;

const response = await fetch(url, {
  headers: { 'X-API-Key': ZENOVAY_API_KEY },
});

const data = await response.json();
return NextResponse.json(data);
}

分析概要データの取得

/analytics/:websiteIdエンドポイントは、サマリー統計と日次の内訳を返します:

lib/zenovay.tsTypeScript
const API_BASE = '/api/analytics'; // Your proxy endpoint

export interface AnalyticsOverview {
website: { id: string; domain: string; name: string };
time_range: string;
summary: {
  total_visitors: number;
  total_page_views: number;
  unique_visitors: number;
};
daily_stats: Array<{
  date: string;
  total_visitors: number;
  page_views: number;
  unique_visitors: number;
}>;
}

export async function fetchOverview(
websiteId: string,
range = '7d'
): Promise<AnalyticsOverview> {
const res = await fetch(
  `${API_BASE}?websiteId=${websiteId}&range=${range}`
);
const json = await res.json();

if (!json.success) throw new Error(json.error?.message || 'API error');
return json.data;
}

export async function fetchCountries(websiteId: string, range = '7d') {
const res = await fetch(
  `${API_BASE}?websiteId=${websiteId}&endpoint=countries&range=${range}`
);
const json = await res.json();
if (!json.success) throw new Error(json.error?.message || 'API error');
return json.data.countries;
}

export async function fetchTechnology(websiteId: string, range = '7d') {
const res = await fetch(
  `${API_BASE}?websiteId=${websiteId}&endpoint=technology&range=${range}`
);
const json = await res.json();
if (!json.success) throw new Error(json.error?.message || 'API error');
return json.data;
}

訪問者チャートの構築

分析概要のdaily_stats配列を使用して時系列チャートを構築します。以下は単純なHTMLキャンバスの例ですが、任意のチャートライブラリ(Chart.js、Rechartsなど)を使用できます:

components/VisitorChart.tsxTSX
'use client';

import { useEffect, useState } from 'react';
import { fetchOverview, type AnalyticsOverview } from '@/lib/zenovay';

interface Props {
websiteId: string;
range?: string;
}

export function VisitorChart({ websiteId, range = '7d' }: Props) {
const [data, setData] = useState<AnalyticsOverview | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetchOverview(websiteId, range)
    .then(setData)
    .finally(() => setLoading(false));
}, [websiteId, range]);

if (loading) return <div>Loading chart...</div>;
if (!data) return <div>No data available</div>;

const { daily_stats, summary } = data;
const maxViews = Math.max(...daily_stats.map(d => d.page_views));

return (
  <div>
    <div style={{ display: 'flex', gap: '2rem', marginBottom: '1rem' }}>
      <div>
        <strong>{summary.total_visitors.toLocaleString()}</strong>
        <div>Total Visitors</div>
      </div>
      <div>
        <strong>{summary.total_page_views.toLocaleString()}</strong>
        <div>Page Views</div>
      </div>
      <div>
        <strong>{summary.unique_visitors.toLocaleString()}</strong>
        <div>Unique Visitors</div>
      </div>
    </div>

    <div style={{ display: 'flex', alignItems: 'flex-end', gap: '2px', height: '200px' }}>
      {daily_stats.map((day) => (
        <div
          key={day.date}
          title={`${day.date}: ${day.page_views} views`}
          style={{
            flex: 1,
            height: `${(day.page_views / maxViews) * 100}%`,
            backgroundColor: '#3b82f6',
            borderRadius: '2px 2px 0 0',
            minHeight: '2px',
          }}
        />
      ))}
    </div>

    <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.75rem' }}>
      <span>{daily_stats[0]?.date}</span>
      <span>{daily_stats[daily_stats.length - 1]?.date}</span>
    </div>
  </div>
);
}

地理的マップ

/analytics/:websiteId/countriesエンドポイントを使用して訪問者の場所を表示します。レスポンスには、任意のマップライブラリで機能する国コードが含まれています:

components/CountryTable.tsxTSX
'use client';

import { useEffect, useState } from 'react';
import { fetchCountries } from '@/lib/zenovay';

interface Country {
country_code: string;
country_name: string;
visitors: number;
avg_value_score: number;
}

export function CountryTable({ websiteId }: { websiteId: string }) {
const [countries, setCountries] = useState<Country[]>([]);

useEffect(() => {
  fetchCountries(websiteId, '30d').then(setCountries);
}, [websiteId]);

const total = countries.reduce((sum, c) => sum + c.visitors, 0);

return (
  <table style={{ width: '100%', borderCollapse: 'collapse' }}>
    <thead>
      <tr>
        <th style={{ textAlign: 'left' }}>Country</th>
        <th style={{ textAlign: 'right' }}>Visitors</th>
        <th style={{ textAlign: 'right' }}>%</th>
      </tr>
    </thead>
    <tbody>
      {countries.map((country) => (
        <tr key={country.country_code}>
          <td>{country.country_name}</td>
          <td style={{ textAlign: 'right' }}>{country.visitors.toLocaleString()}</td>
          <td style={{ textAlign: 'right' }}>
            {total > 0 ? ((country.visitors / total) * 100).toFixed(1) : 0}%
          </td>
        </tr>
      ))}
    </tbody>
  </table>
);
}

インタラクティブマップの場合は、react-simple-mapsまたはMapbox GL JSなどのライブラリとともに国データをペアリングしてください。country_codeフィールドはISO 3166-1 alpha-2コードを使用しており、これらのライブラリで直接理解されます。


テクノロジーの内訳

/analytics/:websiteId/technologyからデバイス、ブラウザ、OS統計を表示します:

components/TechBreakdown.tsxTSX
'use client';

import { useEffect, useState } from 'react';
import { fetchTechnology } from '@/lib/zenovay';

interface TechItem {
name: string;
count: number;
percentage: number;
}

function BarList({ items, label }: { items: TechItem[]; label: string }) {
return (
  <div>
    <h3>{label}</h3>
    {items.slice(0, 5).map((item) => (
      <div key={item.name} style={{ marginBottom: '0.5rem' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
          <span>{item.name}</span>
          <span>{item.percentage}%</span>
        </div>
        <div style={{ background: '#e5e7eb', borderRadius: '4px', height: '8px' }}>
          <div
            style={{
              width: `${item.percentage}%`,
              background: '#3b82f6',
              borderRadius: '4px',
              height: '100%',
            }}
          />
        </div>
      </div>
    ))}
  </div>
);
}

export function TechBreakdown({ websiteId }: { websiteId: string }) {
const [tech, setTech] = useState<{
  devices: TechItem[];
  browsers: TechItem[];
  operating_systems: TechItem[];
} | null>(null);

useEffect(() => {
  fetchTechnology(websiteId).then(setTech);
}, [websiteId]);

if (!tech) return <div>Loading...</div>;

return (
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '2rem' }}>
    <BarList items={tech.devices} label="Devices" />
    <BarList items={tech.browsers} label="Browsers" />
    <BarList items={tech.operating_systems} label="Operating Systems" />
  </div>
);
}

リアルタイム訪問者数

ライブ訪問者エンドポイントは公開であり、認証は必要ありません:

GET https://api.zenovay.com/e/live/YOUR_TRACKING_CODE

これにより、ブラウザから直接コールするのが安全です。ライブカウンターの場合は10秒ごとにポーリングします:

components/LiveVisitors.tsxTSX
'use client';

import { useEffect, useState } from 'react';

export function LiveVisitors({ trackingCode }: { trackingCode: string }) {
const [count, setCount] = useState<number | null>(null);

useEffect(() => {
  const fetchLive = async () => {
    try {
      const res = await fetch(
        `https://api.zenovay.com/e/live/${trackingCode}`
      );
      const data = await res.json();
      setCount(data.visitors ?? data.data?.visitors ?? 0);
    } catch {
      // Silently fail -- keep last known count
    }
  };

  fetchLive();
  const interval = setInterval(fetchLive, 10_000);
  return () => clearInterval(interval);
}, [trackingCode]);

return (
  <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
    <span
      style={{
        width: '8px',
        height: '8px',
        borderRadius: '50%',
        backgroundColor: count !== null ? '#22c55e' : '#9ca3af',
        display: 'inline-block',
      }}
    />
    <span>
      {count !== null ? count.toLocaleString() : '--'} visitor{count !== 1 ? 's' : ''} online
    </span>
  </div>
);
}

すべてを組み合わせる

すべてのコンポーネントを組み合わせた完全なダッシュボードページを以下に示します:

app/dashboard/page.tsxTSX
import { VisitorChart } from '@/components/VisitorChart';
import { CountryTable } from '@/components/CountryTable';
import { TechBreakdown } from '@/components/TechBreakdown';
import { LiveVisitors } from '@/components/LiveVisitors';

const WEBSITE_ID = 'ws_abc123';         // Your Zenovay website ID
const TRACKING_CODE = 'ZV_XXXXXXXXXXX';  // Your tracking code

export default function DashboardPage() {
return (
  <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '2rem' }}>
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
      <h1>Analytics Dashboard</h1>
      <LiveVisitors trackingCode={TRACKING_CODE} />
    </div>

    <section style={{ marginTop: '2rem' }}>
      <h2>Visitor Trends</h2>
      <VisitorChart websiteId={WEBSITE_ID} range="30d" />
    </section>

    <section style={{ marginTop: '2rem' }}>
      <h2>Technology</h2>
      <TechBreakdown websiteId={WEBSITE_ID} />
    </section>

    <section style={{ marginTop: '2rem' }}>
      <h2>Top Countries</h2>
      <CountryTable websiteId={WEBSITE_ID} />
    </section>
  </div>
);
}

次のステップ

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