From e26892f3757e2832417b633e157b17b80dbc6f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Dzier=C5=BCek?= Date: Mon, 31 Aug 2026 21:28:55 +0200 Subject: [PATCH] fix(facebook): use live post insight metrics and read Reels from /video_insights Two separate breakages in Graph API v25: 1. Feed posts asked for the retired `post_impressions` / `post_impressions_unique` pair. Including a retired metric fails the whole request, so no metrics came back at all. Replaced with the current names: `post_media_view` (views) and `post_total_media_view_unique` (reach). 2. Facebook Reels have no /insights edge at all, so every Reel returned an error. Their metrics live on /video_insights, which returns a sensible default set when called without a `metric` parameter. Also maps raw metric names to the same readable labels the other analytics services use (Reach / Views / Likes / Clicks), instead of ucfirst() on the raw Graph key - the new names do not read well otherwise ("Post total media view unique"). Reel reaction counts arrive as a dictionary keyed by reaction type, so those values are summed. --- app/Services/Social/FacebookAnalytics.php | 68 ++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/app/Services/Social/FacebookAnalytics.php b/app/Services/Social/FacebookAnalytics.php index 79d0ad84b..9c88a4b22 100644 --- a/app/Services/Social/FacebookAnalytics.php +++ b/app/Services/Social/FacebookAnalytics.php @@ -4,6 +4,7 @@ namespace App\Services\Social; +use App\Enums\PostPlatform\ContentType; use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Services\Social\Concerns\HasSocialHttpClient; @@ -46,9 +47,13 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array return ['unsupported' => true, 'reason' => 'missing_post_id']; } + if ($postPlatform->content_type === ContentType::FacebookReel) { + return $this->fetchReelMetrics($postPlatform, $account); + } + $response = $this->socialHttp() ->get("{$this->baseUrl}/{$postPlatform->platform_post_id}/insights", [ - 'metric' => 'post_impressions,post_impressions_unique,post_reactions_like_total,post_clicks', + 'metric' => 'post_total_media_view_unique,post_media_view,post_reactions_like_total,post_clicks', 'access_token' => $account->access_token, ]); @@ -60,17 +65,76 @@ public function fetchPostMetrics(PostPlatform $postPlatform): array return ['unsupported' => true, 'reason' => 'api_error']; } + $labels = [ + 'post_total_media_view_unique' => 'Reach', + 'post_media_view' => 'Views', + 'post_reactions_like_total' => 'Likes', + 'post_clicks' => 'Clicks', + ]; + $insights = data_get($response->json(), 'data', []); return collect($insights) ->map(fn (array $item) => [ - 'label' => ucfirst(str_replace('_', ' ', data_get($item, 'name', ''))), + 'label' => $labels[data_get($item, 'name', '')] ?? ucfirst(str_replace('_', ' ', data_get($item, 'name', ''))), 'value' => (int) data_get($item, 'values.0.value', 0), ]) ->values() ->all(); } + /** + * Facebook Reels have no /insights edge - their metrics live on /video_insights. + * Called without a `metric` parameter, Graph returns the default set (including + * blue_reels_play_count, post_impressions_unique, post_video_avg_time_watched). + * Those are mapped to readable labels; dictionary values (reactions per type) are summed. + */ + private function fetchReelMetrics(PostPlatform $postPlatform, SocialAccount $account): array + { + $response = $this->socialHttp() + ->get("{$this->baseUrl}/{$postPlatform->platform_post_id}/video_insights", [ + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + Log::warning('Facebook reel metrics fetch failed', [ + 'body' => $this->redactResponseBody($response->body()), + ]); + + return ['unsupported' => true, 'reason' => 'api_error']; + } + + $labels = [ + 'post_impressions_unique' => 'Reach', + 'blue_reels_play_count' => 'Plays', + 'fb_reels_total_plays' => 'Total plays', + 'fb_reels_replay_count' => 'Replays', + 'post_video_likes_by_reaction_type' => 'Likes', + 'post_video_avg_time_watched' => 'Avg watch time ms', + 'post_video_view_time' => 'View time ms', + 'post_video_followers' => 'Follows', + ]; + + $insights = data_get($response->json(), 'data', []); + + return collect($insights) + ->filter(fn (array $item) => isset($labels[data_get($item, 'name', '')])) + ->map(function (array $item) use ($labels) { + $value = data_get($item, 'values.0.value', 0); + + if (! is_numeric($value)) { + $value = array_sum(array_map('intval', (array) $value)); + } + + return [ + 'label' => $labels[data_get($item, 'name', '')], + 'value' => (int) $value, + ]; + }) + ->values() + ->all(); + } + private function fetchMetricsFromApi(SocialAccount $account, CarbonInterface $since, CarbonInterface $until): array { $this->accessToken = $account->access_token;