Skip to Content
IssuesChannel Health Scores Missing for Website, LandingPage, and Google Ads

Channel Health Scores Missing for Website, LandingPage, and Google Ads

Date: 2026-05-05
Status: ✅ Fixed
Affected channels: Website, LandingPage, Google Ads


Symptoms

The Channel Health dashboard (/channels/health) showed “Not scored yet” for Website, LandingPage, Google Ads, and GitHub channels even when those channels were connected and had data. Facebook, GSC, GA, GBP, and LinkedIn displayed scores correctly.

Google Ads was the most confusing case — it showed “Last scored: 5 May 2026” (the insight job had run) but still displayed “Not scored yet” (the score itself was null).


Root Causes

Three separate bugs — one per channel type.

1. Google Ads — insight ran but healthScore was null

google-ads-insights.worker.ts called runInsightWorker without passing healthScore or healthDimensions. The computeCampaignScore function existed in health-score-calculator.ts but was never called by this worker. So the ChannelInsight record was created with status: "done" and a completedAt timestamp, but healthScore was always null.

Files:

  • packages/agents/src/workers/insights/google-ads-insights.worker.ts

2. Website — scores in WebsiteInsight, not ChannelInsight

website-insights.worker.ts writes scores to the WebsiteInsight model (overallScore, seoScore, contentScore, technicalScore). The buildHealthSummary function in channel-health-summary.ts only reads from ChannelInsight. So even after a successful crawl and analysis, the score was invisible to the health dashboard.

Files:

  • apps/api/src/routers/tenant/channel-health-summary.ts

3. LandingPage — scores in LandingPage, not ChannelInsight

landing-page-scorer.worker.ts writes scores to individual LandingPage records (seoScore, conversionScore, brandScore). There is no connectedChannelId on LandingPage and no corresponding ChannelInsight record. buildHealthSummary had no way to surface these scores.

Files:

  • apps/api/src/routers/tenant/channel-health-summary.ts

4. GitHub — by design, no fix needed

No insights worker exists for GitHub and there are no meaningful performance metrics to score. “Not scored yet” is the correct state.


Fixes

Fix 1: Google Ads worker — compute and pass healthScore

Added computeHealthScore() to google-ads-insights.worker.ts:

  1. Moved syncCampaignMetrics to run before runInsightWorker (it was previously a post-insight side-effect), so the DB has fresh CampaignMetrics rows before the score is computed.
  2. Added computeHealthScore() which queries CampaignExternalMapping to get campaign IDs for the channel, then aggregates CampaignMetrics for current (last 30d), previous (30–60d), and all-time windows.
  3. Calls computeCampaignScore() from health-score-calculator.ts and passes the result as healthScore + healthDimensions to runInsightWorker.

If no CampaignExternalMapping records exist (no campaigns synced yet), computeCampaignScore receives all-zero inputs and returns overall: 10 (“Insufficient data” path).

Fix 2 & 3: buildHealthSummary — add type-specific score lookups

Refactored buildHealthSummary in apps/api/src/routers/tenant/channel-health-summary.ts:

  1. Load channels first, then extract websiteChannelIds and check hasLandingPageChannel.
  2. Add two parallel queries alongside the existing ChannelInsight query:
    • WebsiteInsight: fetch the latest completed record per connectedChannelId for Website channels, select overallScore + completedAt.
    • LandingPage: aggregate _avg { seoScore, conversionScore } and _max { brandScoredAt } for approved pages with a non-null seoScore.
  3. Added resolveScore(channel) helper that picks the right data source per channel type:
    • "Website"WebsiteInsight.overallScore
    • "LandingPage"Math.round((avgSeoScore + avgConversionScore) / 2)
    • everything else → ChannelInsight.healthScore
  4. Used resolveScore() throughout: in connectedScores (overall average), channelData mapping, and the attention items loop.

This also fixes the same gap in the DM portal — it imports buildHealthSummary from the same file.


Testing

  • Website score appears immediately once any WebsiteInsight record with status: "done" exists for the channel.
  • LandingPage score appears once any landing page reaches status: "approved" and has been scored by the landing-page-scorer worker.
  • Google Ads score appears on the next insight run (re-enqueue agent__google-ads-insights or wait for the scheduler).

Pattern Note

Channels whose insight workers use a separate model (not ChannelInsight) must be handled explicitly in buildHealthSummary. Current non-standard channels:

Channel typeScore modelScore field(s)
WebsiteWebsiteInsightoverallScore
LandingPageLandingPageavg of seoScore + conversionScore
all othersChannelInsighthealthScore

Any future channel whose worker does not write to ChannelInsight must add a corresponding branch to resolveScore().

© 2026 Leadmetrics — Internal use only