Graph API v26.0 Migration (Facebook & Instagram)
2026-10-13 — what changed in facebook.plugin.ts / instagram.plugin.ts, why, and how it works.
Why
Production audit logs showed three failures after the last deploy:
| Log | Error | Cause |
|---|---|---|
facebook-fetch-error | (#190) This method must be called with a Page Access Token | a user token sent to a Page endpoint |
facebook-fetch-error | code 190 / subcode 2069032 "A Page access token is required … new Pages experience" | same |
instagram-get-stats-media | shares is not defined | the deploy removed const shares = … but left totalShares += shares |
Plus Top Posts had gone stale and was YouTube-only.
What changed
1. One version constant
GRAPH_API_VERSION = 'v26.0' lives in packages/shared/shared/platformConstants.ts. Both plugins import it, so the version can never drift between files again. The OAuth dialog URL was also moved off the stray v20.0.
2. Metric deprecations (the real killer)
Since 2025-11-15 these are removed for all API versions and, because one bad metric fails a whole /insights batch with (#100), they silently zeroed post insights:
| Removed | v26 replacement |
|---|---|
page_impressions* / post_impressions* | page_media_view / post_media_view |
page_fans | page_follows |
post_engaged_users | (none — use engagement totals) |
page_views | page_media_view (note: page_views_total is still valid) |
facebook-insights.ts now batches only valid metrics, and post_total_media_view_unique (a total_value metric) is read from total_value.value — it used to always read 0.
Rule of thumb: never batch a newly-added metric with existing ones without checking the deprecated-metrics list.
3. Page Access Token
Page Insights/feed need a Page token from a user holding the Page's ANALYZE task. isPageAccessTokenError() now classifies #190 / 2069032 and logs an actionable "reconnect the Page" hint instead of a bare error. Automatic healing landed later (2026-10-17) — see Provider Token Error Triage: StatsCollector force-renews on extra.needsPageToken, AutoPost retries once on a token-auth failure, and every user-token mirror onto a Page row is gated by usesPageToken. That is what turned the recurring #190 batch off.
4. Instagram host routing + valid metrics
The plugin hardcoded graph.facebook.com, but the app also supports Instagram Login (graph.instagram.com). _getGraphApiUrl() picks the host from the token already in the URL: IG… → graph.instagram.com, EAA… → graph.facebook.com. No schema change.
Account insights — only documented metrics, and always with a range. getStatistic() now makes one account-level read:
GET /{ig-user}/insights?metric=reach,views,likes,comments,saves,shares,accounts_engaged,total_interactions
&period=day&metric_type=total_value&since=…&until=…&access_token=…Three traps this fixes:
profile_visitsandnew_followsare not valid account metrics → the call always failed.follower_count/online_followersare withheld for accounts with <100 followers → "Follower insights error" on small accounts.impressionswas removed (2025-04-21). Withoutsince/untilthe endpoint only looks back 24h and returns all-zero totals, so the range is mandatory.
Media insights — never batch the removed metric. impressions is deprecated for media created after 2024-07-02; keeping it in the fallback ladder forced a wasted retry (and a warning log) per media. The ladder is now reach,views,likes,comments,shares,saved, and account stats no longer call /{ig-media}/insights at all — one collect dropped from ~40 requests to 3.
Note: views/reach are now a 30-day window (matching the "Last 30 Days" card) instead of the old lifetime sum of per-media views, so the number will be smaller and more accurate.
5. Top Posts ranking
normalizePostInsights.total used to sum every metric, including views/reach. That made YouTube's raw viewCount dominate every ranking. total is now engagement only (ENGAGEMENT_KEYS = likes, comments, shares, saves, quotes, bookmarks, retweets, reposts). And a post is only marked failed when insights are a bare fallback — a partial set stays visible.
6. Facebook: "Views" ≠ Reach ≠ Page Views
These three v26 metrics look alike but are different numbers:
| Dashboard label | Metric | Meaning |
|---|---|---|
| Views | page_media_view | content played/displayed — matches the Professional Dashboard "Views" |
| Unique Viewers | page_total_media_view_unique | unique accounts reached → mapped to reach |
| Page Views | page_views_total | times the Page profile was viewed |
getPageInsights/analytics request all four and label them distinctly. getStatistic maps views = page_media_view, reach = page_total_media_view_unique, and exposes pageProfileViews/uniqueViewers in extra. Previously page_views_total was mislabelled "Reach" and page_media_view wasn't requested at all, so the dashboard "Views" could never match.
Pull Views directly:
GET /v26.0/{page-id}/insights?metric=page_media_view&period=day&since=…&until=…&access_token=…
# needs: read_insights + pages_read_engagement, Page token from a user with the ANALYZE task7. YouTube: 30-day views need the Analytics API
YouTube's Data API channels.statistics.viewCount is lifetime only — there is no date range. Windowed views come from a different API:
GET https://youtubeanalytics.googleapis.com/v2/reports
?ids=channel==MINE&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&metrics=viewsYouTubePlugin.fetchViewsLast30d() calls this and sets engagement.views, keeping the lifetime number in extra.viewCount and the old last-50 sum in extra.recentUploadViews. It requires the yt-analytics.readonly scope (added to the youtube provider in auth.ts); until every YouTube account reconnects, the call returns null and the plugin falls back — collecting never fails.
Note: YouTube's
youtube.force-ssldoes not cover Analytics. If Google ever rejects the combined consent, split Analytics into its own incremental provider (as done for Google Drive / Business).
How to verify
# 1. no stray versions
grep -nE "v2[0-9]\.0" packages/db/server/services/plugins/facebook.plugin.ts \
packages/db/server/services/plugins/instagram.plugin.ts
# 2. insights ladder contract
node --test packages/db/server/services/plugins/facebook-insights.test.ts
# 3. real accounts (staging): getStatistic + getPostInsights non-zero, no #190Related
- Facebook & Instagram Setup
- Meta App Review
- PRD:
.aiContext/PRD-GRAPH-API-V26-FB-IG.md