Skip to content

Reporting Dashboard - Humology Integration Progress

Master document for tracking reporting dashboard improvements, with focus on Bunny Analytics integration and UX improvements for Humology's workflow.

Done

  • [x] Group-level checkboxes - Creative groups have three-state checkboxes (unchecked/partial/checked) with toggle-all behavior
  • [x] Selection count on groups - Group headers show "X/Y" when partially selected, plain count when none
  • [x] Impression counts in picker - Lazy-loaded, shows lifetime impressions per creative and group, formatted K/M
  • [x] "Active only" filter - Filters to creatives with >0 impressions, waits for data to load, auto-expands
  • [x] Bunny standard metrics in MetricPicker - "Bunny Analytics" section with standard metrics
  • [x] CSV parser: standard column extraction - STANDARD_COL_TO_KEY mapping for standard Bunny columns
  • [x] Fix: standard Bunny metrics sent as custom_metric[] params - Separated BUNNY_STANDARD_KEYS from custom API keys
  • [x] Fix: 204 No Content crash - Guard in bunnyCsvParser for undefined CSV response
  • [x] Dynamic widget layout - ReportViewer builds widgets from selected metrics, not hardcoded views
  • [x] Chart tooltip UX - interaction: { mode: 'index', intersect: false } for hover anywhere
  • [x] Bunny metric codes in URL - BUNNY_METRICS constant, b_* codes, proper encode/decode
  • [x] Chart zoom/scroll - chartjs-plugin-zoom with useChartZoom composable, ChartZoomControls component
  • [x] Bunny "no data" messaging - BunnyNoDataState component, hasBunnyNoData in composables, per-card "No data"
  • [x] Fix: Interaction Rate 1256.9% - Computed metrics (interaction-rate, ctr) were multiplied by 100 twice. Now return ratio, formatPercentage handles display.
  • [x] Fix: Viewable Impressions and Seconds in View = 0 - Wrong key names (dashes vs underscores). Also: aggregation API requires metric[]=base not individual keys. useCounts and useBuckets now always request 'base'.
  • [x] Computed metrics infrastructure - COMPUTED_METRICS and COMPUTED_DEPENDENCIES in metricCodes.ts. useReportTotals auto-fetches dependencies and calculates derived values.
  • [x] Engagement funnel improvements - Value placed outside bar with gap (no text crash). Logarithmic scale for visible sub-metrics. Percentage rate labels next to counts.
  • [x] Toggle all / Clear all per metric group - "Select all" and "Clear" buttons on Standard and Bunny Analytics sections in MetricPicker
  • [x] Zero-value KPI card fading - Cards with 0 values get opacity 0.4 so they don't dominate the report
  • [x] Custom metric discovery - "+ Add custom metric" panel discovers per-creative custom metrics (e.g. scratch_done, scratch_start)
  • [x] Trim BUNNY_METRICS to actual CSV columns - Backend CSV only has 9 metric columns (not 22). Removed 13 ghost metrics that were showing as empty cards.
  • [x] Expand STANDARD_METRICS to all 18 aggregation API metrics - Added interacted, continued, creative_actions, link_clicks, header_clicks, background_clicks, reached_end, plays, watched_25/50/75/100. All available via metric[]=base.
  • [x] Metric source investigation - Confirmed: aggregation API has 16 metrics, Bunny parquet has all of them + extras (clicks, seconds_total_active, custom_metrics). Backend CSV only exposes 9. During migration, use aggregation for full coverage, Bunny for extras + custom metrics.
  • [x] Fix: duplicate Bunny API calls - Each WidgetCard created its own useReportTotals → N separate API calls. Refactored: WidgetGrid fetches ALL metrics + rate denominators in one useReportTotals call, passes data down via props.
  • [x] Fix: Bunny KPI cards showing "0" instead of "No data" - WidgetCard now receives bunnyNoData prop from WidgetGrid and shows "No data" for unavailable bunny metrics.
  • [x] Fix: chart showing only 2 days - findActiveRange used absolute threshold (300) that filtered out low-volume days. Changed to > 0 so all days with data are visible.
  • [x] Fix: chart pill buttons using wrong data source - ALL_CHARTABLE dedup always preferred aggregation source. Now respects the report's data source: if report uses bunny metrics, chart pill buttons use bunny too.
  • [x] Creative ID search in ResourcePicker - Search by creative ID and name, shows #ID next to creative name
  • [x] Creative IDs on report cards - Reports list shows creative IDs (e.g. #94281) extracted from URL
  • [x] Root cause: Bunny low impression counts - Logs-Parser bunny_analytics branch writes flat file, backend expects hive-partitioned glob. See bunny-parquet-mismatch.md.

TODO

Widget CRUD System (brainstorming in progress, 2026-06-29)

Design decisions made so far:

Workflow:

  1. MetricPicker remains the "starter" -- bulk-select metrics and creatives
  2. In the report view, toggle Edit mode (button, logged-in users only) to adjust
  3. Edit mode enables: remove individual cards (X), reorder (DnD or arrows), add new metrics (inline "+ Add metric" dropdown), change widget type per metric
  4. Preview button in edit mode shows the report as the customer will see it (no edit controls)
  5. Shared/public report links are always read-only (no edit controls)

Persistence:

  • Widget layout encoded in URL query params (not localStorage)
  • Compact format, e.g. ?w=imp:kpi,b_clicks:hidden,custom:scratch_done:kpi
  • Shared links preserve exact layout

Widget types (phased):

  • Phase 1: KPI cards with CRUD (add, remove, reorder, hide)
  • Phase 2: Multiple widget types per metric (KPI card, line chart, bar chart)
  • Phase 3: DnD repositioning, resize

Design decisions (completed 2026-06-29):

  • [x] Reordering: Full DnD with 12-column grid slots
  • [x] URL format: ?layout=metricCode:colspan,... (order = position)
  • [x] "+ Add metric": Inline dropdown in last grid cell, grouped by source
  • [x] Remove = omit from layout and metrics (no hidden state in phase 1)
  • [x] Grid applies app-wide: Reports list also uses 12-column grid (4 cols per report card)
  • [x] No native browser dialogs -- custom confirm/alert components
  • [x] Delete buttons as icons, not text (fixes overflow on report cards)
  • [x] All dropdowns/controls integrated with app design tokens

Full spec: todos/Humology/widget-crud-spec.md

Metric Source Unification

  • [x] Investigate Bunny vs aggregation overlap - Confirmed: near-complete overlap. Aggregation API (Cloudflare) is prod, Bunny is migrating. Both compute same metrics from same events, different pipelines.
  • [ ] Go all-in on Bunny - Kevin confirms all creatives use Bunny for 6+ months. Plan: hide CF metrics behind devTools flag (localhost only), use Bunny as sole data source. Eliminates source conflicts (e.g. 75K bunny imps vs 553K CF imps showing side-by-side, rates calculated across sources).
  • [ ] Unified MetricPicker - Merge into single metric list when Bunny CSV expands. For now, keep separation since aggregation covers more metrics.

UX Improvements

  • [ ] Date sorting in ResourcePicker - Sort by created_at so newer creatives appear first. DataTreeNode has created_at.
  • [ ] Campaign-level impression counts - Sum impressions in campaign row without expanding
  • [ ] Visual indicator for inactive creatives - Faded styling for 0 impressions
  • [ ] Custom metric discovery indicator - Show which creatives have Bunny custom metrics
  • [ ] Impression threshold filter - Filter picker to creatives with >1000 impressions (meaningful data only)

Bunny Analytics

  • [ ] Blocked: Parquet mismatch - Logs-Parser writes flat file, backend expects hive-partitioned. Kevin investigating. See bunny-parquet-mismatch.md
  • [ ] Blocked: Expand backend CSV - Once parquet fix lands, add missing columns (viewable_impressions, started, continued, reached_end, plays, etc.) to backend SELECT
  • [ ] Test Bunny clicks end-to-end - Verify clicks, watched_25, etc. display correctly with a creative that has real engagement data
  • [ ] Per-creative "no Bunny data" state - Show which creatives in a report lack Bunny data
  • [ ] Bunny custom metric preview in picker - Show available metric names per creative before adding

Code Quality

  • [ ] Extract shared types - DateRange duplicated 4-5x, DataTreeNode types duplicated in 3 files
  • [ ] Convention alignment - Arrow functions, type instead of interface
  • [ ] Component splitting - ResourcePicker ~700 lines, split into composables/components
  • [ ] Add CLAUDE.md / ESLint / Prettier config - Align with AF conventions
  • [ ] Stores reactivity - Better reactive wiring for metric changes through widgets

Backlog (from Linear)

  • [ ] CAV-170: Report links - Shareable report URLs
  • [ ] CAV-171: Report link management UI - Manage saved/shared reports
  • [ ] CAV-168: Export - PDF/CSV export of reports

Architecture Notes

Two Data Pipelines (verified 2026-06-29)

Pipeline 1: Aggregation API (Cloudflare Worker relay)

  • Backend proxies requests via RelayService.ts to external cavai.aggregation.url
  • Routes: /api/aggregation-api/counts, /api/aggregation-api/buckets
  • metric[]=base returns all 16 metrics at once
  • Supports: impressions, interacted, started, continued, creative_actions, link_clicks, header_clicks, background_clicks, reached_end, plays, watched_25, watched_50, watched_75, watched_100, viewable_impressions, seconds_in_view
  • This is the OLD pipeline -- data comes from Cloudflare Worker log processing

Pipeline 2: Bunny Analytics (DuckDB parquet files)

  • Logs-Parser processes raw Bunny CDN logs -> decoded.parquet -> aggregated.parquet -> hourly_summary parquet
  • Backend reads parquet via DuckDB and returns CSV: /api/analytics/bunny/hourly-summary/:creative_id
  • Custom metrics discovered per-creative: /api/analytics/bunny/metrics/:creative_id
  • This is the NEW pipeline -- replacing Cloudflare

Parquet Has ALL Metrics (verified 2026-06-29)

The hourly_summary parquet files (written by Logs-Parser Aggregator.js) contain ALL these columns:

impressions, renderizations, viewable_impressions, interacted, started,
continued, seconds_in_view, seconds_to_active, creative_actions,
link_clicks, reached_end, header_clicks, background_clicks, plays,
watched_25, watched_50, watched_75, watched_100,
analytics_allowed_count, analytics_disallowed_count,
seconds_total_active, clicks, custom_metrics (MAP)

But the backend CSV endpoint (analytics.ts) only SELECTs 9 of them:

impressions, link_clicks, creative_actions (as "Total Interactions"),
clicks, watched_25/50/75/100, seconds_total_active

Missing from CSV but present in parquet:viewable_impressions, started, continued, reached_end, plays, header_clicks, background_clicks, seconds_in_view, seconds_to_active, renderizations, analytics_allowed_count, analytics_disallowed_count

Overlap Between Sources

MetricAggregation APIBunny ParquetBunny CSV
impressionsxxx
link_clicksxxx
watched_25/50/75/100xxx
interactedxx (as creative_actions>0)-
startedxx-
continuedxx-
viewable_impressionsxx-
seconds_in_viewxx-
creative_actionsxx-
reached_endxx-
playsxx-
header_clicksxx-
background_clicksxx-
seconds_to_active-x-
seconds_total_active-xx
clicks-xx
renderizations-x-
analytics_allowed/disallowed-x-
custom_metrics-xx

Next Step: Expand Backend CSV

The fastest path to "all metrics available" is adding the missing columns to the backend's DuckDB SELECT in analytics.ts. No new infrastructure needed -- the data is already in parquet. Then the reporting frontend can show everything from Bunny alone, and we can phase out the aggregation API relay.

Other Notes

  • interaction-rate and ctr are computed client-side from raw counts
  • 204 No Content from Bunny = creative has no Bunny analytics (expected for older creatives)
  • Standard Bunny CSV columns always present; only custom metrics need custom_metric[] API params

PRs That Built This Pipeline

  • AB#553 - Initial Bunny DuckDB analytics route
  • AB#554 - Added Seconds Total Active
  • AB#557 - Added Clicks + Companion Creative Clicks
  • LP#37 - DuckDB optimization, parquet pipeline
  • LP#35 - Manual date processing for historic data

Debugging

Open Chrome DevTools Network tab:

  • GET /api/analytics/bunny/hourly-summary/{creative_id} -- CSV (or 204 if no data)
  • GET /api/analytics/bunny/metrics/{creative_id} -- { metrics: [...] }
  • GET /api/aggregation-api/counts?...&metric[]=base -- all standard metrics
  • 204 + Content-Disposition: attachment; filename="86403.csv" = no Bunny data for this creative

Internal documentation