From 8bcda4e3d88a130b1067c015d2eb3396608da42b Mon Sep 17 00:00:00 2001 From: Craig Phares Date: Sun, 20 Sep 2026 12:29:10 -0400 Subject: [PATCH] Drop the insight metrics Meta retired The API validates the whole metric list before it reads any of it, so a single retired name answers 400 for the request and costs every metric for that media kind. `plays` had been kept on video and reel on the belief that v21 still served it. It does not: production rejects it, and the error names every value the API will accept. `exits` is absent from that list too, so the story set was failing the same way. The practical effect was that every feed video and every reel published through the app recorded no insights at all, while carousels and images came back whole. The ingestion job rescues the error and logs it, so nothing surfaced. Video counts plays as `views` now, verified against a real feed video on the same call that rejected `plays`. Stories drop `exits` and keep what the list allows; `navigation` is what replaced it, but it reports through a breakdown this client does not ask for yet, so that waits on a live story to check the shape. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 14 ++++++++--- lib/instagram_graph_api/metrics.rb | 31 ++++++++++++++++++------ spec/instagram_graph_api/metrics_spec.rb | 24 ++++++++++++------ 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c629c6f..de90209 100644 --- a/README.md +++ b/README.md @@ -119,16 +119,22 @@ The metric whitelist lives in `InstagramGraphAPI::Metrics`: ```ruby InstagramGraphAPI::Metrics::MEDIA_INSIGHT_METRICS[:reel] # => ["reach", "likes", "comments", "shares", "saved", "total_interactions", -# "plays", "views", "ig_reels_avg_watch_time", "ig_reels_video_view_total_time"] +# "views", "ig_reels_avg_watch_time", "ig_reels_video_view_total_time"] InstagramGraphAPI::Metrics::ACCOUNT_INSIGHT_METRICS # => ["views", "profile_views", "follower_count", "accounts_engaged", # "total_interactions", "reach", "likes", "comments", "shares", "saves"] ``` -Deprecated names (`impressions`, `engagement`, `video_views`) are -intentionally absent — the Graph v21 schema renamed/retired them in -2024–2025. +Deprecated names (`impressions`, `engagement`, `video_views`, `plays`, +`exits`) are intentionally absent — the Graph schema renamed or retired +them between 2024 and 2026. + +Keep the list narrow. The API validates the whole `metric` list before it +reads any of it, so a single retired name answers 400 for the request and +costs every metric for that media kind. When one is rejected the error +names every value the API will accept, which is the list to widen +against. ### Comments + replies diff --git a/lib/instagram_graph_api/metrics.rb b/lib/instagram_graph_api/metrics.rb index ab2ebc8..fc56f66 100644 --- a/lib/instagram_graph_api/metrics.rb +++ b/lib/instagram_graph_api/metrics.rb @@ -1,19 +1,34 @@ # frozen_string_literal: true module InstagramGraphAPI - # Whitelist of insight metric names the Instagram Graph API (v21+) - # returns for each media kind. Names that Meta retired during the 2024 - # and 2025 schema cleanups — `impressions`, `engagement`, - # `video_views` — are intentionally absent. `plays` is still supported - # for feed videos and reels in v21+ and is included accordingly. + # Whitelist of insight metric names the Instagram Graph API returns for + # each media kind. Names that Meta retired during the 2024 and 2025 + # schema cleanups — `impressions`, `engagement`, `video_views`, + # `plays` — are intentionally absent. + # + # A rejected name fails the whole request, not just its own column: the + # API validates the metric list up front and answers 400 for all of it. + # One retired name therefore costs every metric for that media kind, so + # this list is only ever widened against a verified response. + # + # `plays` was dropped 2026-09-20. It had been kept on video and reel on + # the belief that v21 still served it; production rejects it with + # `metric[6] must be one of the following values: ...`, and that list + # does not contain it. Video takes `views` in its place, verified + # against a real feed video on the same call. The same error names + # every metric the API will accept, and `exits` is absent from it too, + # which is why the story set no longer asks for it. # # Consumed by the Rails ingestion layer in phase 5b. module Metrics MEDIA_INSIGHT_METRICS = { image: %w[reach likes comments shares saved total_interactions].freeze, - video: %w[reach likes comments shares saved total_interactions plays].freeze, - reel: %w[reach likes comments shares saved total_interactions plays views ig_reels_avg_watch_time ig_reels_video_view_total_time].freeze, - story: %w[reach replies exits views].freeze, + video: %w[reach likes comments shares saved total_interactions views].freeze, + reel: %w[reach likes comments shares saved total_interactions views ig_reels_avg_watch_time ig_reels_video_view_total_time].freeze, + # `navigation` is what replaced `exits`, but it reports through a + # breakdown this client does not ask for yet, so it is left out + # until a live story can be used to check the shape that comes back. + story: %w[reach replies views].freeze, carousel: %w[reach likes comments shares saved total_interactions views].freeze }.freeze diff --git a/spec/instagram_graph_api/metrics_spec.rb b/spec/instagram_graph_api/metrics_spec.rb index 307a54a..c0bf571 100644 --- a/spec/instagram_graph_api/metrics_spec.rb +++ b/spec/instagram_graph_api/metrics_spec.rb @@ -19,22 +19,30 @@ end it 'uses story-specific metrics' do - expect(described_class::MEDIA_INSIGHT_METRICS[:story]).to match_array(%w[reach replies exits views]) + expect(described_class::MEDIA_INSIGHT_METRICS[:story]).to match_array(%w[reach replies views]) end it 'excludes metrics deprecated in the 2024-2025 Graph schema' do all_metrics = described_class::MEDIA_INSIGHT_METRICS.values.flatten.uniq expect(all_metrics).not_to include('impressions') # retired across all media kinds expect(all_metrics).not_to include('engagement') # retired - expect(all_metrics).not_to include('video_views') # renamed → `views`/`plays` + expect(all_metrics).not_to include('video_views') # renamed → `views` end - it 'keeps `plays` on the kinds where Graph v21 still supports it' do - expect(described_class::MEDIA_INSIGHT_METRICS[:video]).to include('plays') - expect(described_class::MEDIA_INSIGHT_METRICS[:reel]).to include('plays') - %i[image story carousel].each do |kind| - expect(described_class::MEDIA_INSIGHT_METRICS[kind]).not_to include('plays') - end + # A retired name is not a missing column, it is a 400 for the whole + # request. While `plays` sat in the video list, every feed video and + # every reel published through the app recorded no insights at all, + # and the ingestion job logged the failure and moved on. `exits` was + # doing the same to stories. + it 'asks for no metric the API has retired out from under it' do + all_metrics = described_class::MEDIA_INSIGHT_METRICS.values.flatten.uniq + expect(all_metrics).not_to include('plays') + expect(all_metrics).not_to include('exits') + end + + it 'counts video plays as `views`, the name that replaced them' do + expect(described_class::MEDIA_INSIGHT_METRICS[:video]).to include('views') + expect(described_class::MEDIA_INSIGHT_METRICS[:reel]).to include('views') end end