Skip to main content
The Analytics API provides endpoints for tracking user events and retrieving governance analytics data.

Track Event

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name": "proposal_viewed",
    "event_data": {
      "proposal_id": "123",
      "user_address": "0x1234...5678"
    }
  }' \
  "https://vote.ens.domains/api/analytics/track"
Location in code: src/app/api/analytics/track/route.ts:3

Request Body

event_name
string
required
Name of the event to track (e.g., proposal_viewed, vote_cast, delegate_changed)
event_data
object
required
Event-specific data as a JSON object. Structure varies by event type.

Response

success
boolean
Whether the event was successfully tracked

Example Response

{
  "success": true
}

Event Types

Common event types you can track:
  • proposal_viewed - User viewed a proposal
  • proposal_created - User created a proposal
  • vote_cast - User cast a vote
  • delegate_changed - User changed their delegate
  • statement_updated - Delegate updated their statement
  • page_view - Page view event
  • wallet_connected - User connected their wallet

Example Event Data

Proposal Viewed

{
  "event_name": "proposal_viewed",
  "event_data": {
    "proposal_id": "123",
    "user_address": "0x1234...5678",
    "proposal_type": "STANDARD",
    "timestamp": "2024-01-15T14:23:45Z"
  }
}

Vote Cast

{
  "event_name": "vote_cast",
  "event_data": {
    "proposal_id": "123",
    "voter_address": "0x1234...5678",
    "support": 1,
    "voting_power": "5000000000000000000000",
    "timestamp": "2024-01-15T14:23:45Z"
  }
}

Delegate Changed

{
  "event_name": "delegate_changed",
  "event_data": {
    "delegator": "0x1234...5678",
    "from_delegate": "0xabcd...ef01",
    "to_delegate": "0x9876...5432",
    "timestamp": "2024-01-15T14:23:45Z"
  }
}

Get Proposal Vote Counts

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://vote.ens.domains/api/analytics/vote"
Location in code: src/app/api/analytics/vote/route.ts:3

Response

Returns aggregated vote count statistics across all proposals.
{
  "total_votes": 15234,
  "total_voters": 3421,
  "average_votes_per_proposal": 152.34,
  "proposals_with_votes": 100,
  "breakdown": {
    "for_votes": 9821,
    "against_votes": 4123,
    "abstain_votes": 1290
  }
}

Get Top Delegate Weights

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://vote.ens.domains/api/analytics/top/delegates"
Location in code: src/app/api/analytics/top/delegates/route.ts:3

Response

Returns the top delegates by voting power with their weights.
[
  {
    "address": "0x1234567890abcdef1234567890abcdef12345678",
    "voting_power": "5000000000000000000000",
    "percentage_of_supply": 2.5,
    "rank": 1,
    "num_of_delegators": 142
  },
  {
    "address": "0x2345678901abcdef2345678901abcdef23456789",
    "voting_power": "4500000000000000000000",
    "percentage_of_supply": 2.25,
    "rank": 2,
    "num_of_delegators": 128
  }
]

Get Metric Time Series

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://vote.ens.domains/api/analytics/metric/voting_power/daily"
Location in code: src/app/api/analytics/metric/[metric_id]/[frequency]/route.ts:3

Path Parameters

metric_id
string
required
Metric identifier. Available metrics:
  • voting_power - Total voting power over time
  • active_delegates - Number of active delegates
  • proposals_created - Proposals created over time
  • votes_cast - Votes cast over time
  • participation_rate - Voter participation rate
  • treasury_balance - Treasury balance (if available)
frequency
string
required
Time frequency for data points:
  • hourly - Hourly data points
  • daily - Daily aggregation
  • weekly - Weekly aggregation
  • monthly - Monthly aggregation

Response

metric_id
string
The metric identifier
frequency
string
Time frequency used
data
array
Array of time series data points

Example Response

{
  "metric_id": "voting_power",
  "frequency": "daily",
  "data": [
    {
      "timestamp": "2024-01-01T00:00:00Z",
      "value": 195000000000000000000000000
    },
    {
      "timestamp": "2024-01-02T00:00:00Z",
      "value": 196500000000000000000000000
    },
    {
      "timestamp": "2024-01-03T00:00:00Z",
      "value": 198000000000000000000000000
    }
  ]
}

Analytics Database Schema

analyticsEvent Table

CREATE TABLE analytics_event (
  id SERIAL PRIMARY KEY,
  event_name VARCHAR(255) NOT NULL,
  event_data JSONB NOT NULL,
  dao_slug VARCHAR(50) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_analytics_event_name ON analytics_event(event_name);
CREATE INDEX idx_analytics_event_dao ON analytics_event(dao_slug);
CREATE INDEX idx_analytics_event_created ON analytics_event(created_at);

Event Tracking Implementation

Location in code: src/app/api/analytics/track/route.ts:3
export async function POST(request: NextRequest) {
  const { slug } = Tenant.current();
  const authResponse = await authenticateApiUser(request);

  if (!authResponse.authenticated) {
    return new Response(authResponse.failReason, { status: 401 });
  }

  const { event_name, event_data } = await request.json();

  await prismaWeb2Client.analyticsEvent.create({
    data: {
      event_name,
      event_data,
      dao_slug: slug,
    },
  });
  
  return NextResponse.json({ success: true });
}

Environment Variables

Enable Analytics Capture

NEXT_PUBLIC_ENABLE_BI_METRICS_CAPTURE=true
Enables analytics event tracking throughout the application.

DataDog Integration

DD_API_KEY=your-datadog-api-key
DD_APP_KEY=your-datadog-app-key
ENABLE_DD_METRICS=true
Optional: Enables DataDog monitoring integration for analytics.

Use Cases

Track User Interactions

// Track when a user views a proposal
await fetch('/api/analytics/track', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    event_name: 'proposal_viewed',
    event_data: {
      proposal_id: proposalId,
      user_address: userAddress
    }
  })
});

Monitor Voting Activity

// Get current vote statistics
const stats = await fetch('/api/analytics/vote', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(r => r.json());

console.log(`Total votes: ${stats.total_votes}`);
console.log(`Average per proposal: ${stats.average_votes_per_proposal}`);

Analyze Delegate Distribution

// Get top delegates by voting power
const topDelegates = await fetch('/api/analytics/top/delegates', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
}).then(r => r.json());

const top10VotingPower = topDelegates
  .slice(0, 10)
  .reduce((sum, d) => sum + parseFloat(d.voting_power), 0);

console.log(`Top 10 delegates control: ${top10VotingPower}`);

Track Metrics Over Time

// Get daily voting power trends
const timeSeries = await fetch(
  '/api/analytics/metric/voting_power/daily',
  { headers: { 'Authorization': `Bearer ${apiKey}` } }
).then(r => r.json());

// Calculate growth
const first = timeSeries.data[0].value;
const last = timeSeries.data[timeSeries.data.length - 1].value;
const growth = ((last - first) / first * 100).toFixed(2);

console.log(`Voting power growth: ${growth}%`);

Analytics Queries

Query Events by Name

SELECT 
  event_name,
  COUNT(*) as count,
  DATE_TRUNC('day', created_at) as day
FROM analytics_event
WHERE dao_slug = 'ens'
  AND event_name = 'proposal_viewed'
GROUP BY event_name, day
ORDER BY day DESC;

Query Events by User

SELECT 
  event_name,
  event_data->>'user_address' as user,
  COUNT(*) as count
FROM analytics_event
WHERE dao_slug = 'ens'
  AND event_data->>'user_address' = '0x1234...5678'
GROUP BY event_name, user;
SELECT 
  event_data->>'proposal_id' as proposal_id,
  COUNT(*) as views
FROM analytics_event
WHERE dao_slug = 'ens'
  AND event_name = 'proposal_viewed'
  AND created_at > NOW() - INTERVAL '7 days'
GROUP BY proposal_id
ORDER BY views DESC
LIMIT 10;

Error Responses

Missing Event Name

{
  "error": "event_name is required",
  "status": 400
}

Invalid Event Data

{
  "error": "event_data must be a valid JSON object",
  "status": 400
}

Authentication Required

{
  "error": "Missing or invalid bearer token",
  "status": 401
}

Database Error

{
  "error": "Internal server error: Database connection failed",
  "status": 500
}

Best Practices

  1. Consistent Event Names: Use a naming convention like resource_action (e.g., proposal_viewed, vote_cast)
  2. Include Timestamps: Always include timestamps in event_data for time-based analysis
  3. Structured Data: Keep event_data structured and consistent for easier querying
  4. Privacy Compliance: Don’t track personally identifiable information without consent
  5. Rate Limiting: Be mindful of analytics tracking volume
  6. Error Handling: Handle failed tracking gracefully without breaking user experience

Performance Considerations

Batch Events

For high-volume tracking, consider batching events:
const events = [
  { event_name: 'proposal_viewed', event_data: {...} },
  { event_name: 'delegate_viewed', event_data: {...} },
];

// Process events in batch
await Promise.all(
  events.map(event => 
    fetch('/api/analytics/track', {
      method: 'POST',
      body: JSON.stringify(event)
    })
  )
);

Async Tracking

Track events asynchronously to avoid blocking user interactions:
// Don't await analytics calls
fetch('/api/analytics/track', {
  method: 'POST',
  body: JSON.stringify(event)
}).catch(err => console.error('Analytics failed:', err));

Database Indexing

Ensure proper indexes for common queries:
CREATE INDEX idx_event_data_proposal_id 
ON analytics_event((event_data->>'proposal_id'));

CREATE INDEX idx_event_data_user_address 
ON analytics_event((event_data->>'user_address'));

Build docs developers (and LLMs) love