Skip to content

fix(analytics): grant Instagram Direct the insights scope it was gated out of - #148

Merged
JanSchm merged 1 commit into
mainfrom
claude/instagram-direct-analytics-238930
Aug 13, 2026
Merged

fix(analytics): grant Instagram Direct the insights scope it was gated out of#148
JanSchm merged 1 commit into
mainfrom
claude/instagram-direct-analytics-238930

Conversation

@JanSchm

@JanSchm JanSchm commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Makes instagram_business_manage_insights an 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_insights approved on the Meta app. Two independent causes:

  1. The platform was switched off in the admin. That copy is the CAUSE_DISABLED branch of analytics_availability(), which fires only for an AnalyticsPlatformConfig row with is_enabled=False. That's a deployment setting and stays one — no migration flips it here.

  2. 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.py has always requested instagram_manage_insights unconditionally on the Facebook-Page path; the Direct connector now matches. The trade-off, recorded in SocialProvider.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 a post_save receiver rather than ModelAdmin.save_model, so a shell save() and update_or_create are covered too; it skips created (a first row states the status quo — a missing row already reads as enabled), skips platforms in NO_ANALYTICS_PLATFORMS via the shared services.analytics_availability predicate, dedupes, and defers the enqueue to commit.

backfill_account_analytics now forces today's re-fetch: _sync_account_metrics skips 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 in apps/analytics/tests/test_signals.py (9 cases), rewritten apps/social_accounts/tests/test_admin.py using RequestFactory with real messages storage, plus two force_today regression tests in apps/analytics/tests/test_tasks.py.

The four new guards were mutation-checked rather than trusted green — removing the availability gate, un-skipping created, dropping force_today, and swapping transaction.on_commit for a direct call each fail exactly one test.

End-to-end, after deploy:

  1. Django admin → Analytics platforms → tick instagram_login → Save. Expect an info message naming the queued account(s).
  2. Open the analytics page for that account. The "aren't available" state should be gone; the amber Reconnect for analytics banner appears once the worker runs the backfill (the stored token predates the scope).
  3. Click Reconnect. The Instagram consent screen should now list the insights permission. Approve — analytics_needs_reconnect clears and a 90-day backfill runs.
  4. If the queue is slow: python manage.py backfill_analytics --account-id <uuid>. To isolate the API side: python manage.py instagram_review_test_calls.

Checklist

  • Tests pass (pytest) — 1440 passed
  • Lint passes (ruff check . and ruff format --check .)
  • Documentation updated (if applicable) — SocialProvider.analytics_only_scopes and _apply_analytics_scope_flag docstrings now name which providers honor the gate and why Instagram Direct opts out; README already listed the permission as required

🤖 Generated with Claude Code

…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
JanSchm merged commit d938464 into main Aug 13, 2026
4 of 5 checks passed
@JanSchm
JanSchm deleted the claude/instagram-direct-analytics-238930 branch August 13, 2026 08:17
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant