When your edge infrastructure spans dozens of services and millions of requests, standard analytics views become insufficient. As technical leaders managing complex PropTech platforms, we need granular visibility into performance metrics, user behavior, and system health. Cloudflare Analytics offers powerful customization capabilities that most developers never fully leverage, leaving critical insights buried in data.
This comprehensive guide will transform how you approach monitoring and observability at the edge, giving you the tools to build dashboards that actually matter for your business.
Understanding Cloudflare Analytics Architecture
Cloudflare Analytics operates on a distributed data pipeline that processes billions of events daily across their global network. Unlike traditional analytics platforms that sample data, Cloudflare captures every request, providing complete visibility into your edge traffic.
The Analytics Data Flow
Every request through Cloudflare's network generates multiple data points that flow through their analytics pipeline:
- Request metadata: HTTP status codes, response times, cache status
- Geographic data: Country, city, data center location
- Security events: Bot scores, firewall triggers, DDoS mitigation
- Performance metrics: Time to first byte, total request duration
- Custom dimensions: Worker execution data, custom headers
This data becomes available through multiple interfaces: the dashboard UI, GraphQL Analytics API, and Workers Analytics API. Each serves different use cases, with custom dashboards requiring deep integration with these APIs.
Analytics Data Retention and Granularity
Cloudflare maintains analytics data at different retention periods and granularities:
- Real-time data: 1-minute granularity, available for 6 hours
- Hourly aggregates: Available for 30 days
- Daily aggregates: Available for 1 year
- Raw logs: Available through Logpush for unlimited retention
Understanding these limitations shapes how you architect custom dashboards. For real-time monitoring, you'll work with high-granularity, short-retention data. For trend analysis, you'll need longer-term aggregates.
Workers Analytics Integration
Workers Analytics extends the standard analytics pipeline by allowing custom event tracking within your edge functions. This creates opportunities for business-specific metrics that traditional web analytics can't capture.
export default {
class="kw">async fetch(request: Request, env: Env, ctx: ExecutionContext) {
class="kw">const startTime = Date.now();
// Your application logic here
class="kw">const response = class="kw">await handleRequest(request);
// Custom analytics event
ctx.waitUntil(
fetch(039;https://api.cloudflare.com/client/v4/accounts/{account_id}/analytics/events039;, {
method: 039;POST039;,
headers: {
039;Authorization039;: Bearer ${env.CF_API_TOKEN},
039;Content-Type039;: 039;application/json039;
},
body: JSON.stringify({
timestamp: new Date().toISOString(),
dimension1: 039;property_search039;,
dimension2: request.cf?.city || 039;unknown039;,
metric1: Date.now() - startTime,
metric2: response.status
})
})
);
class="kw">return response;
}
};
Building Custom Dashboard Foundations
Custom dashboards require a systematic approach to data collection, processing, and visualization. The foundation involves setting up proper data sources, defining key metrics, and establishing update mechanisms.
API Authentication and Setup
Cloudflare's Analytics API requires proper authentication and understanding of rate limits. Most custom dashboard implementations fail because they don't properly handle API constraints.
class CloudflareAnalytics {
private apiToken: string;
private accountId: string;
private zoneId: string;
constructor(config: AnalyticsConfig) {
this.apiToken = config.apiToken;
this.accountId = config.accountId;
this.zoneId = config.zoneId;
}
class="kw">async queryGraphQL(query: string, variables: Record<string, any> = {}) {
class="kw">const response = class="kw">await fetch(039;https://api.cloudflare.com/client/v4/graphql039;, {
method: 039;POST039;,
headers: {
039;Authorization039;: Bearer ${this.apiToken},
039;Content-Type039;: 039;application/json039;,
039;X-Auth-Email039;: process.env.CF_EMAIL || 039;039;
},
body: JSON.stringify({ query, variables })
});
class="kw">if (!response.ok) {
throw new Error(GraphQL query failed: ${response.statusText});
}
class="kw">return response.json();
}
}
Defining Custom Metrics
Effective dashboards focus on metrics that drive business decisions. For PropTech applications, this might include property search performance, geographic user distribution, or booking conversion rates.
query PropertySearchMetrics($zoneTag: string!, $since: Time!, $until: Time!) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequests1hGroups(
filter: {
datetime_geq: $since
datetime_leq: $until
requestSource: "eyeball"
}
orderBy: [datetime_ASC]
limit: 1000
) {
dimensions {
datetime
clientCountryName
clientRequestPath
edgeResponseStatus
}
sum {
requests
bytes
}
avg {
edgeTimeToFirstByteMs
}
}
}
}
}
Real-Time Data Processing
Custom dashboards need efficient data processing to handle Cloudflare's high-volume analytics streams. This involves implementing proper caching, data aggregation, and update mechanisms.
class DashboardDataProcessor {
private cache = new Map<string, CachedMetric>();
private updateInterval = 60000; // 1 minute
class="kw">async processMetrics(rawData: CloudflareAnalyticsResponse) {
class="kw">const processed = {
totalRequests: this.aggregateRequests(rawData),
averageResponseTime: this.calculateAverageResponseTime(rawData),
topCountries: this.getTopCountries(rawData),
errorRates: this.calculateErrorRates(rawData),
cacheHitRatio: this.calculateCacheRatio(rawData)
};
// Update cache with processed metrics
this.cache.set(039;current_metrics039;, {
data: processed,
timestamp: Date.now(),
ttl: this.updateInterval
});
class="kw">return processed;
}
private aggregateRequests(data: CloudflareAnalyticsResponse): number {
class="kw">return data.viewer.zones[0].httpRequests1hGroups.reduce(
(total, group) => total + group.sum.requests, 0
);
}
private calculateAverageResponseTime(data: CloudflareAnalyticsResponse): number {
class="kw">const groups = data.viewer.zones[0].httpRequests1hGroups;
class="kw">const totalTime = groups.reduce(
(sum, group) => sum + (group.avg.edgeTimeToFirstByteMs * group.sum.requests), 0
);
class="kw">const totalRequests = groups.reduce(
(sum, group) => sum + group.sum.requests, 0
);
class="kw">return totalRequests > 0 ? totalTime / totalRequests : 0;
}
}
Advanced Dashboard Implementation
Building production-ready custom dashboards requires sophisticated data handling, visualization components, and real-time update mechanisms. This section covers advanced implementation patterns that scale with enterprise requirements.
Multi-Zone Analytics Aggregation
Enterprise PropTech platforms often manage multiple domains and zones. Custom dashboards need to aggregate metrics across these zones while maintaining granular visibility.
class MultiZoneAnalytics extends CloudflareAnalytics {
private zones: ZoneConfig[];
class="kw">async aggregateMultiZoneMetrics(timeRange: TimeRange): Promise<AggregatedMetrics> {
class="kw">const zonePromises = this.zones.map(class="kw">async (zone) => {
class="kw">const query =
query ZoneMetrics($zoneTag: string!, $since: Time!, $until: Time!) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
httpRequests1hGroups(
filter: {
datetime_geq: $since
datetime_leq: $until
}
limit: 10000
) {
dimensions {
datetime
clientCountryName
edgeResponseStatus
}
sum {
requests
bytes
cachedRequests
}
avg {
edgeTimeToFirstByteMs
}
}
}
}
}
;
class="kw">const result = class="kw">await this.queryGraphQL(query, {
zoneTag: zone.tag,
since: timeRange.start.toISOString(),
until: timeRange.end.toISOString()
});
class="kw">return {
zoneName: zone.name,
data: result.data.viewer.zones[0].httpRequests1hGroups
};
});
class="kw">const zoneResults = class="kw">await Promise.all(zonePromises);
class="kw">return this.mergeZoneMetrics(zoneResults);
}
private mergeZoneMetrics(zoneResults: ZoneMetrics[]): AggregatedMetrics {
// Complex aggregation logic class="kw">for combining zone data
class="kw">const merged = {
totalRequests: 0,
totalBytes: 0,
averageResponseTime: 0,
cacheHitRatio: 0,
errorRate: 0,
zoneBreakdown: new Map<string, ZoneMetrics>()
};
zoneResults.forEach(zone => {
class="kw">const zoneTotal = zone.data.reduce((sum, group) => {
class="kw">return {
requests: sum.requests + group.sum.requests,
bytes: sum.bytes + group.sum.bytes,
cachedRequests: sum.cachedRequests + group.sum.cachedRequests,
totalResponseTime: sum.totalResponseTime +
(group.avg.edgeTimeToFirstByteMs * group.sum.requests)
};
}, { requests: 0, bytes: 0, cachedRequests: 0, totalResponseTime: 0 });
merged.totalRequests += zoneTotal.requests;
merged.totalBytes += zoneTotal.bytes;
merged.zoneBreakdown.set(zone.zoneName, zoneTotal);
});
class="kw">return merged;
}
}
Workers Analytics Integration
Workers Analytics provides custom event tracking capabilities that enable business-specific metrics collection. This is particularly valuable for PropTech applications tracking user interactions, search queries, and booking flows.
// Worker script class="kw">for custom analytics
export default {
class="kw">async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
class="kw">const analytics = new WorkerAnalytics(env.ANALYTICS_TOKEN);
class="kw">const startTime = performance.now();
try {
// Extract business context from request
class="kw">const url = new URL(request.url);
class="kw">const userAgent = request.headers.get(039;user-agent039;) || 039;039;;
class="kw">const referer = request.headers.get(039;referer039;) || 039;039;;
// Handle the request
class="kw">const response = class="kw">await this.handlePropertySearch(request, env);
class="kw">const endTime = performance.now();
// Track custom business metrics
ctx.waitUntil(
analytics.trackEvent({
eventType: 039;property_search039;,
searchQuery: url.searchParams.get(039;query039;) || 039;039;,
location: url.searchParams.get(039;location039;) || 039;039;,
priceRange: url.searchParams.get(039;price_range039;) || 039;039;,
resultCount: parseInt(response.headers.get(039;x-result-count039;) || 039;0039;),
responseTime: endTime - startTime,
userCountry: request.cf?.country || 039;unknown039;,
userCity: request.cf?.city || 039;unknown039;,
deviceType: this.detectDeviceType(userAgent),
timestamp: new Date().toISOString()
})
);
class="kw">return response;
} catch (error) {
// Track errors
ctx.waitUntil(
analytics.trackEvent({
eventType: 039;search_error039;,
errorMessage: error.message,
responseTime: performance.now() - startTime,
timestamp: new Date().toISOString()
})
);
throw error;
}
},
class="kw">async handlePropertySearch(request: Request, env: Env): Promise<Response> {
// Your property search logic here
class="kw">const searchResults = class="kw">await this.queryPropertyDatabase(request, env);
class="kw">return new Response(JSON.stringify(searchResults), {
headers: {
039;content-type039;: 039;application/json039;,
039;x-result-count039;: searchResults.length.toString()
}
});
}
};
class WorkerAnalytics {
constructor(private token: string) {}
class="kw">async trackEvent(event: CustomAnalyticsEvent): Promise<void> {
class="kw">await fetch(039;https://api.cloudflare.com/client/v4/accounts/{account_id}/workers/analytics/events039;, {
method: 039;POST039;,
headers: {
039;Authorization039;: Bearer ${this.token},
039;Content-Type039;: 039;application/json039;
},
body: JSON.stringify({
events: [event]
})
});
}
}
Dashboard Visualization Components
Custom dashboards require efficient rendering of complex data visualizations. This implementation shows how to build reusable components for common PropTech metrics.
class PropertyAnalyticsDashboard {
private analytics: MultiZoneAnalytics;
private updateInterval: NodeJS.Timeout;
constructor(analyticsClient: MultiZoneAnalytics) {
this.analytics = analyticsClient;
}
class="kw">async renderDashboard(): Promise<DashboardData> {
class="kw">const [trafficMetrics, searchMetrics, conversionMetrics] = class="kw">await Promise.all([
this.getTrafficMetrics(),
this.getSearchMetrics(),
this.getConversionMetrics()
]);
class="kw">return {
traffic: this.formatTrafficData(trafficMetrics),
searches: this.formatSearchData(searchMetrics),
conversions: this.formatConversionData(conversionMetrics),
lastUpdated: new Date().toISOString()
};
}
private class="kw">async getSearchMetrics(): Promise<SearchMetrics> {
class="kw">const query =
query SearchAnalytics($since: Time!, $until: Time!) {
viewer {
accounts(filter: { accountTag: "{account_id}" }) {
workersInvocationsAdaptive(
filter: {
datetime_geq: $since
datetime_leq: $until
scriptName: "property-search-worker"
}
limit: 1000
) {
dimensions {
datetime
status
}
sum {
requests
duration
}
}
}
}
}
;
class="kw">const timeRange = {
start: new Date(Date.now() - 24 60 60 * 1000), // 24 hours ago
end: new Date()
};
class="kw">const result = class="kw">await this.analytics.queryGraphQL(query, {
since: timeRange.start.toISOString(),
until: timeRange.end.toISOString()
});
class="kw">return this.processSearchMetrics(result.data.viewer.accounts[0].workersInvocationsAdaptive);
}
}
Best Practices and Optimization
Production custom dashboards require careful attention to performance, reliability, and maintainability. These best practices ensure your analytics infrastructure scales with your PropTech platform's growth.
Efficient Data Caching Strategies
Cloudflare Analytics APIs have rate limits and latency considerations. Implementing intelligent caching reduces API calls while maintaining data freshness for critical metrics.
class AnalyticsCache {
private redis: RedisClient;
private cacheTTL = {
realtime: 60, // 1 minute class="kw">for real-time data
hourly: 900, // 15 minutes class="kw">for hourly aggregates
daily: 3600, // 1 hour class="kw">for daily summaries
trends: 21600 // 6 hours class="kw">for trend analysis
};
class="kw">async getCachedMetrics(cacheKey: string, dataType: keyof typeof this.cacheTTL): Promise<any | null> {
try {
class="kw">const cached = class="kw">await this.redis.get(cacheKey);
class="kw">if (cached) {
class="kw">const parsed = JSON.parse(cached);
class="kw">if (Date.now() - parsed.timestamp < this.cacheTTL[dataType] * 1000) {
class="kw">return parsed.data;
}
}
} catch (error) {
console.warn(039;Cache read failed:039;, error);
}
class="kw">return null;
}
class="kw">async setCachedMetrics(cacheKey: string, data: any): Promise<void> {
try {
class="kw">await this.redis.set(cacheKey, JSON.stringify({
data,
timestamp: Date.now()
}));
} catch (error) {
console.warn(039;Cache write failed:039;, error);
}
}
}
Error Handling and Resilience
Robust dashboards handle API failures gracefully and provide meaningful feedback to users when data is unavailable.
class ResilientAnalyticsClient {
private maxRetries = 3;
private baseDelay = 1000;
class="kw">async queryWithRetry<T>(queryFn: () => Promise<T>): Promise<T | null> {
class="kw">let lastError: Error;
class="kw">for (class="kw">let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
class="kw">return class="kw">await queryFn();
} catch (error) {
lastError = error as Error;
class="kw">if (this.isRetryableError(error)) {
class="kw">const delay = this.baseDelay * Math.pow(2, attempt);
class="kw">await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// Non-retryable error, fail immediately
break;
}
}
console.error(039;Analytics query failed after retries:039;, lastError);
class="kw">return null;
}
private isRetryableError(error: any): boolean {
class="kw">if (error.status >= 500) class="kw">return true; // Server errors
class="kw">if (error.status === 429) class="kw">return true; // Rate limiting
class="kw">if (error.code === 039;ETIMEDOUT039;) class="kw">return true; // Timeouts
class="kw">return false;
}
}
Performance Monitoring
Monitor your custom dashboard's own performance to ensure it doesn't become a bottleneck. Track API response times, cache hit rates, and rendering performance.
class DashboardMetrics {
private metrics = {
apiResponseTimes: [] as number[],
cacheHitRate: 0,
errorRate: 0,
activeConnections: 0
};
trackApiCall(duration: number, success: boolean): void {
this.metrics.apiResponseTimes.push(duration);
class="kw">if (this.metrics.apiResponseTimes.length > 100) {
this.metrics.apiResponseTimes.shift();
}
class="kw">if (!success) {
this.metrics.errorRate = this.calculateErrorRate();
}
}
getPerformanceReport(): PerformanceReport {
class="kw">const responseTimes = this.metrics.apiResponseTimes;
class="kw">return {
averageApiResponseTime: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
p95ApiResponseTime: this.percentile(responseTimes, 95),
cacheHitRate: this.metrics.cacheHitRate,
errorRate: this.metrics.errorRate,
activeConnections: this.metrics.activeConnections
};
}
}
Scaling and Production Deployment
Deploying custom Cloudflare Analytics dashboards in production environments requires careful consideration of architecture, monitoring, and maintenance strategies. This section covers enterprise-grade deployment patterns.
Microservices Architecture
Large-scale analytics dashboards benefit from a microservices approach, separating data collection, processing, and presentation concerns.
// Data collection service
class AnalyticsCollectorService {
private queue: MessageQueue;
private collectors: Map<string, DataCollector>;
class="kw">async startCollection(): Promise<void> {
// Start multiple collectors class="kw">for different data sources
class="kw">const collectors = [
new CloudflareTrafficCollector(),
new WorkersAnalyticsCollector(),
new CustomEventsCollector()
];
collectors.forEach(collector => {
collector.on(039;data039;, (data) => {
this.queue.publish(039;analytics.raw039;, data);
});
collector.start();
});
}
}
// Data processing service
class AnalyticsProcessorService {
class="kw">async processRawData(rawData: RawAnalyticsData): Promise<ProcessedMetrics> {
class="kw">const processors = [
new TrafficProcessor(),
new ConversionProcessor(),
new PerformanceProcessor()
];
class="kw">const processed = class="kw">await Promise.all(
processors.map(processor => processor.process(rawData))
);
class="kw">return this.combineMetrics(processed);
}
}
Real-Time Updates with WebSockets
Provide real-time dashboard updates using WebSocket connections, enabling immediate visibility into critical metrics changes.
class RealTimeDashboardServer {
private wss: WebSocketServer;
private clients = new Set<WebSocket>();
private metricsCache: DashboardMetrics;
constructor() {
this.wss = new WebSocketServer({ port: 8080 });
this.setupWebSocketHandlers();
this.startMetricsUpdates();
}
private setupWebSocketHandlers(): void {
this.wss.on(039;connection039;, (ws) => {
this.clients.add(ws);
// Send current metrics on connection
ws.send(JSON.stringify({
type: 039;initial_data039;,
data: this.metricsCache.getCurrentMetrics()
}));
ws.on(039;close039;, () => {
this.clients.delete(ws);
});
});
}
private startMetricsUpdates(): void {
setInterval(class="kw">async () => {
try {
class="kw">const updatedMetrics = class="kw">await this.fetchLatestMetrics();
this.broadcastToClients({
type: 039;metrics_update039;,
data: updatedMetrics,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error(039;Failed to update metrics:039;, error);
}
}, 60000); // Update every minute
}
private broadcastToClients(message: any): void {
class="kw">const messageStr = JSON.stringify(message);
this.clients.forEach(client => {
class="kw">if (client.readyState === WebSocket.OPEN) {
client.send(messageStr);
}
});
}
}
At PropTechUSA.ai, we've implemented similar real-time analytics architectures for property management platforms, enabling property managers to monitor tenant portal usage, maintenance request flows, and booking patterns in real-time. This level of visibility has proven crucial for optimizing user experience and operational efficiency.
Automated Alerting and Monitoring
Implement intelligent alerting based on your custom metrics to proactively identify issues before they impact users.
class AlertingEngine {
private rules: AlertRule[];
private notificationChannels: NotificationChannel[];
class="kw">async evaluateMetrics(metrics: DashboardMetrics): Promise<void> {
class="kw">const triggeredAlerts = this.rules
.filter(rule => rule.evaluate(metrics))
.map(rule => this.createAlert(rule, metrics));
class="kw">if (triggeredAlerts.length > 0) {
class="kw">await this.sendAlerts(triggeredAlerts);
}
}
private createAlert(rule: AlertRule, metrics: DashboardMetrics): Alert {
class="kw">return {
id: generateId(),
ruleName: rule.name,
severity: rule.severity,
message: rule.generateMessage(metrics),
timestamp: new Date(),
metrics: rule.extractRelevantMetrics(metrics)
};
}
}
Custom Cloudflare Analytics dashboards represent a powerful opportunity to gain unprecedented visibility into your edge infrastructure performance. By implementing the patterns and practices outlined in this guide, you'll build monitoring solutions that scale with your PropTech platform's growth while providing actionable insights for technical and business decisions.
The combination of Cloudflare's comprehensive analytics APIs, Workers Analytics custom event tracking, and well-architected dashboard implementations creates a monitoring foundation that traditional analytics platforms simply cannot match. Whether you're optimizing property search performance, tracking user engagement across geographic regions, or monitoring conversion funnels, custom dashboards put the most relevant metrics at your fingertips.
Start by implementing basic dashboard components using the code examples provided, then gradually add advanced features like real-time updates, multi-zone aggregation, and intelligent alerting. Your future self will thank you when critical issues are identified and resolved before impacting your users' property search and booking experiences.