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