fix(analytics): grant Instagram Direct the insights scope it was gated out of - #148
Merged
Merged
Conversation
…d out of Connecting an Instagram Business account via Instagram (Direct) and finding "Analytics aren't available for Instagram (Direct)" reads as the platform being unsupported, even with `instagram_business_manage_insights` approved on the Meta app. Two separate things were wrong, and either alone kept analytics dark. The platform was switched off in the Analytics platforms admin — that copy is the `CAUSE_DISABLED` branch, which fires only for an `AnalyticsPlatformConfig` row with `is_enabled=False`. That part is a deployment setting and stays one. The half that code can fix: `instagram_business_manage_insights` was only added to the authorize URL when that same toggle was on. An OAuth grant is frozen at connect time while the toggle can flip afterwards, so an account connected while analytics was off carried a token that could never read `/insights` no matter how the Meta app was configured — and nothing said so. It now sits in `required_scopes` unconditionally, as `providers/instagram.py` has always done with `instagram_manage_insights`. The cost, recorded in `SocialProvider.analytics_only_scopes`: an Instagram app that hasn't added the permission now sees it on the authorize URL. Facebook, TikTok and YouTube keep the conditional gate, where app review is still a real constraint. Switching a platform on now re-checks the accounts already connected to it, rather than leaving the page blank until the hourly cron happens to run, fail on scope, and set `analytics_needs_reconnect`. The backfill either writes the first snapshot or trips the scope error that raises the Reconnect banner. That re-check lives in a `post_save` receiver, not in `ModelAdmin.save_model`, so a shell `save()` and `update_or_create` are covered too — the admin only reports the count it recorded. It is deliberately narrow: - `created` is skipped. A platform with no row already reads as enabled, so a first row with `is_enabled=True` states the status quo rather than changing it. - Platforms in `NO_ANALYTICS_PLATFORMS` are skipped via the shared `services.analytics_availability` predicate the page, the cron and the agent API all use. Queueing Bluesky or DEV.to would insert a task per account that returns immediately — the waste `sync_all_account_analytics` documents having already stamped out — while telling the operator a Reconnect prompt was coming for a platform that can never show analytics at all. - The enqueue is deduped and deferred to commit, so repeated toggles don't stack redundant 90-day fetches and a rolled-back save queues nothing. `backfill_account_analytics` now forces today's re-fetch. `_sync_account_metrics` skips any day that already has a snapshot, so a platform re-enabled on the same day it last synced found today's rows present, never called the provider, and reported success for a token that cannot read insights. The fetch is the only thing that surfaces an insufficient-scope error, so the one-shot paths (connect, reconnect, admin enable, manual backfill) have to make it; the hourly cron still treats existing rows as work done. The disabled-state copy says the account "may also need reconnecting" instead of promising a Reconnect button. That promise held only where the toggle was flipped through a path that fires signals and a worker was up to run the task; advice that holds in every case beats advice that is usually superseded. It carries an id so the view test anchors on structure rather than prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JanSchm
added a commit
that referenced
this pull request
Aug 13, 2026
PR #148 added a pre_save handler that writes `_previously_enabled` onto AnalyticsPlatformConfig instances, but nothing declares the attribute, so mypy rejects the write with attr-defined. main has been red since that merge, which blocks every open PR. Declare it on the model with a None default. The post_save reader already does `getattr(instance, "_previously_enabled", None)`, so the default matches the behaviour that was there for an unstashed instance and nothing changes at runtime. Unrelated to the rest of this branch; folded in to get CI green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Makes
instagram_business_manage_insightsan unconditional scope for the Instagram (Direct) connector, and makes switching a platform on in Analytics platforms re-check the accounts already connected to it.Why?
An Instagram Business account connected via Instagram (Direct) showed "Analytics aren't available for Instagram (Direct)" even with
instagram_business_manage_insightsapproved on the Meta app. Two independent causes:The platform was switched off in the admin. That copy is the
CAUSE_DISABLEDbranch ofanalytics_availability(), which fires only for anAnalyticsPlatformConfigrow withis_enabled=False. That's a deployment setting and stays one — no migration flips it here.The insights scope was never on the token. It was only added to the authorize URL when that same toggle was on. An OAuth grant is frozen at connect time while the toggle can flip afterwards, so an account connected while analytics was off carried a token that could never read
/insights— no matter how the Meta app was configured. Approving the permission in the Meta dashboard genuinely did nothing, which is what made this confusing.providers/instagram.pyhas always requestedinstagram_manage_insightsunconditionally on the Facebook-Page path; the Direct connector now matches. The trade-off, recorded inSocialProvider.analytics_only_scopes: an Instagram app that hasn't added the permission under Permissions and features will now see it on the authorize URL. Facebook, TikTok and YouTube keep the conditional gate, where app review is still a real constraint.The second half closes the window where enabling a platform changed nothing visible until the hourly cron happened to run, fail on scope, and set
analytics_needs_reconnect. The re-check lives in apost_savereceiver rather thanModelAdmin.save_model, so a shellsave()andupdate_or_createare covered too; it skipscreated(a first row states the status quo — a missing row already reads as enabled), skips platforms inNO_ANALYTICS_PLATFORMSvia the sharedservices.analytics_availabilitypredicate, dedupes, and defers the enqueue to commit.backfill_account_analyticsnow forces today's re-fetch:_sync_account_metricsskips days that already have a snapshot, so a platform re-enabled on the same day it last synced never called the provider and reported success for a token that couldn't read insights. That fetch is the only thing that surfaces an insufficient-scope error.The disabled-state copy now says the account "may also need reconnecting" rather than promising a Reconnect button — that promise held only where the toggle was flipped through a signal-firing path and a worker was up to run the task.
How to test
Automated:
pytest(1440 passing). New cover inapps/analytics/tests/test_signals.py(9 cases), rewrittenapps/social_accounts/tests/test_admin.pyusingRequestFactorywith real messages storage, plus twoforce_todayregression tests inapps/analytics/tests/test_tasks.py.The four new guards were mutation-checked rather than trusted green — removing the availability gate, un-skipping
created, droppingforce_today, and swappingtransaction.on_commitfor a direct call each fail exactly one test.End-to-end, after deploy:
instagram_login→ Save. Expect an info message naming the queued account(s).analytics_needs_reconnectclears and a 90-day backfill runs.python manage.py backfill_analytics --account-id <uuid>. To isolate the API side:python manage.py instagram_review_test_calls.Checklist
pytest) — 1440 passedruff check .andruff format --check .)SocialProvider.analytics_only_scopesand_apply_analytics_scope_flagdocstrings now name which providers honor the gate and why Instagram Direct opts out; README already listed the permission as required🤖 Generated with Claude Code