CCFish bridges the gap between Cocos Creator game telemetry and ad network performance data with a real-time analytics dashboard built on Cloudflare Workers, D1, and Charts.js, giving both the dev team and marketing team a single source of truth for every decision. ## The Problem: Disconnected Data Destroys Growth Game development teams and marketing teams speak different languages. Devs talk about retention curves, level completion rates, session length distributions, and in-app purchase conversion funnels. Marketers talk about CPI, ROAS, ad spend efficiency, install volume, and channel-level cost-per-acquisition. These two datasets live in completely different systems — one in the Cocos Creator engine’s local event store (or a custom telemetry pipeline), the other in Meta Ads Manager, TikTok Ads, and Google Ads dashboards. For CCFish — a fishing shooter built with Cocos Creator 2.4.15 and deployed as a Telegram Mini App at playableton-ccfish.pages.dev — this disconnect was actively costing money. When a new level launched, the dev team could see within 48 hours that retention dropped 12% at Level 7. But they had no way to quickly tell marketing to pause ad spend on channels driving users to that level. Meanwhile, the marketing team saw CPI spike 23% on a Meta campaign and knew something was wrong, but had no visibility into whether it was a creative issue, a targeting issue, or a game-side regression. The result was slow, reactive decision-making. By the time both teams synced in a weekly meeting, an entire week’s ad budget (roughly $3,500–$5,000) had already been spent against a funnel that was leaking players. The data lived in silos — and growth suffered for it. ## The Solution: A Unified Real-Time Analytics Dashboard CCFish’s engineering team built a hybrid analytics dashboard that ingests, normalizes, and visualizes both game telemetry and marketing data in a single interface. The core philosophy: **one queryable source of truth, accessible to both devs and marketers, updated in near real-time.** The dashboard achieves this with three architectural pillars: 1. **Cloudflare D1** as the unified event store — game events and ad network data land in the same SQLite-compatible database 2. **A Workers-based API layer** that abstracts the complexity of different data sources behind a consistent REST interface 3. **Charts.js with WebGL rendering** for smooth real-time visualizations that handle 10,000+ data points without jank Both teams access the same dashboard at different zoom levels. Devs drill into per-level retention and session heatmaps. Marketers see ROAS by channel and CPI trends. The magic is that both views query the same underlying D1 database — a change in game performance instantly propagates to marketing’s view, and vice versa. ## Architecture: Cloudflare Workers + D1 + Charts.js The architecture follows a clean three-tier design: ``` [Cocos Engine Telemetry] [Meta/TikTok Ads API] [Google Ads API] | | | v v v +-----------------------------------------------------------+ | Cloudflare Workers (API Gateway) | | - POST /events (game telemetry) | | - POST /ad-spend (ad network callback) | | - GET /dashboard/dev | | - GET /dashboard/marketing | +----------------------------+------------------------------+ | v +-----------------------------------------------------------+ | Cloudflare D1 Database | | - game_events table (retention, sessions, IAP) | | - ad_spend table (impressions, clicks, installs) | | - unified_metrics view (JOIN across both) | +----------------------------+------------------------------+ | v +-----------------------------------------------------------+ | Charts.js + WebGL Frontend Dashboard | | - Dev mode: retention, funnel, session heatmaps | | - Marketing mode: ROAS, CPI, spend by channel | | - Hybrid mode: ROAS vs. retention overlay | +-----------------------------------------------------------+ ``` ### The Workers API The API layer handles two distinct ingestion paths: **Game Telemetry** — The Cocos Creator engine fires events via `fetch()` calls to a Workers endpoint: ```javascript // Workers endpoint: POST /events async function handleGameEvent(request) { const event = await request.json(); const stmt = db.prepare(` INSERT INTO game_events (event_type, player_id, level_id, session_id, metadata, timestamp) VALUES (?, ?, ?, ?, ?, ?) `); await stmt.bind( event.event_type, event.player_id, event.level_id || null, event.session_id, JSON.stringify(event.metadata), event.timestamp ).run(); return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } }); } ``` **Ad Network Data** — Meta, TikTok, and Google Ads APIs push data via scheduled Workers cron jobs every 15 minutes: ```javascript // Cron job: runs every 15 minutes async function syncAdNetworkData(env) { const networks = [ { name: 'meta', api: syncMetaAds }, { name: 'tiktok', api: syncTikTokAds }, { name: 'google', api: syncGoogleAds } ]; for (const network of networks) { const campaigns = await network.api.fetch(env.META_ACCESS_TOKEN); const stmt = env.DB.prepare(` INSERT OR REPLACE INTO ad_spend (date, campaign_id, channel, impressions, clicks, installs, spend) VALUES (?, ?, ?, ?, ?, ?, ?) `); for (const campaign of campaigns) { await stmt.bind( campaign.date, campaign.id, network.name, campaign.impressions, campaign.clicks, campaign.installs, campaign.spend ).run(); } } } ``` ### D1 Storage Schema The key to the unified dashboard is the D1 schema that enables cross-domain queries: ```sql CREATE TABLE game_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, player_id TEXT NOT NULL, level_id TEXT, session_id TEXT NOT NULL, cohort_date TEXT, metadata TEXT, timestamp INTEGER NOT NULL ); CREATE INDEX idx_game_events_type ON game_events(event_type); CREATE INDEX idx_game_events_level ON game_events(level_id); CREATE INDEX idx_game_events_cohort ON game_events(cohort_date); CREATE TABLE ad_spend ( date TEXT NOT NULL, campaign_id TEXT NOT NULL, channel TEXT NOT NULL, impressions INTEGER DEFAULT 0, clicks INTEGER DEFAULT 0, installs INTEGER DEFAULT 0, spend REAL DEFAULT 0, PRIMARY KEY (date, campaign_id) ); CREATE VIEW unified_daily_metrics AS SELECT a.date, a.channel, a.installs, a.spend, a.spend / NULLIF(a.installs, 0) AS cpi, (SELECT COUNT(DISTINCT g.player_id) FROM game_events g WHERE g.event_type = 'session_start' AND g.cohort_date = a.date AND g.timestamp >= unixepoch(a.date) AND g.timestamp < unixepoch(a.date) + 86400 ) AS dau_from_cohort, (SELECT COUNT(DISTINCT g.player_id) FROM game_events g WHERE g.event_type = 'iap_purchase' AND g.cohort_date = a.date ) AS paying_users FROM ad_spend a; ``` This view lets anyone write `SELECT channel, cpi, dau_from_cohort, paying_users FROM unified_daily_metrics` and see which channels deliver the best users — not just the cheapest installs. ### Real-Time Visualization with Charts.js + WebGL The frontend uses Charts.js with the WebGL renderer plugin to handle roughly 200,000 game events and 15,000 ad records per day. Key dashboard panels: | Panel | Data Source | Audience | Refresh | |-------|-------------|----------|---------| | Retention Curve (D1/D7/D30) | game_events | Dev + Marketing | 5 min | | ROAS by Channel | ad_spend + game_events JOIN | Marketing | 15 min | | CPI Trend (7-day) | ad_spend | Marketing | 15 min | | Level Completion Funnel | game_events | Dev | 5 min | | Session Length Distribution | game_events | Dev | 5 min | | Revenue per User by Channel | unified_daily_metrics | Both | 15 min | | Ad Spend Efficiency | unified_daily_metrics | Both | 15 min | The dashboard surfaces **actionable alerts**. When CPI on a channel exceeds 1.5x the 7-day rolling average, the marketing panel highlights it in red. When a level’s completion rate drops below 60%, the dev panel shows a warning with a link to file a bug in the CCFish GitHub repo. ## Implementation: A Single Integration That Connects Everything ### Step 1: Instrument the Cocos Creator Engine CCFish added a lightweight telemetry module to the Cocos Creator 2.4.15 game code. Since the game runs as a Telegram Mini App (WebView-based), it sends events via `fetch()`: ```typescript class GameTelemetry { private apiUrl = 'https://analytics.ccfish.io/events'; private sessionId: string; constructor() { this.sessionId = this.generateSessionId(); } trackEvent(type: string, metadata?: Record<string, any>) { fetch(this.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_type: type, player_id: this.getPlayerId(), session_id: this.sessionId, level_id: GameState.currentLevel, metadata: metadata || {}, timestamp: Math.floor(Date.now() / 1000) }) }).catch(() => {}); } } ``` ### Step 2: Connect Ad Network APIs Each ad network’s API is polled via a Workers cron trigger. The key insight: **normalize all ad data to the same schema** — date, campaign_id, channel, impressions, clicks, installs, spend. This normalization is what makes the unified view possible. ### Step 3: Build the Dashboard Frontend The dashboard is deployed alongside the game on Cloudflare Pages: ```javascript async function loadDashboard() { const devData = await fetch('/dashboard/dev').then(r => r.json()); const mktgData = await fetch('/dashboard/marketing').then(r => r.json()); new Chart(document.getElementById('retentionChart'), { type: 'line', data: { labels: ['D1', 'D3', 'D7', 'D14', 'D30'], datasets: [{ label: 'Current Build', data: devData.retention, borderColor: '#4CAF50', tension: 0.3 }, { label: 'Previous Build', data: devData.retentionPrevious, borderColor: '#FF9800', tension: 0.3, borderDash: [5, 5] }] }, options: { plugins: { webgl: { enable: true } } } }); renderHybridROASChart(mktgData.roasByChannel, devData.
Key Takeaways
- **One source of truth** — Merge game telemetry and ad data at the storage layer, not in a dashboard
- **Build for alerts first** — Real-time anomaly detection saves more money than weekly reporting
- **Normalize ad platform schemas** — Meta, TikTok, and Google all report differently; a unified schema is non-negotiable
- **Hybrid view wins** — The magic happens when devs see marketing data and marketers see game data in the same dashboard