-
Notifications
You must be signed in to change notification settings - Fork 404
fix: Facebook analytics sync #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7e80de7
9e90c2a
0086e62
2a601c1
806eed9
0381692
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -380,6 +380,12 @@ def _write_post_snapshot( | |
| "youtube": frozenset({"watch_time", "avg_view_pct", "shares"}), | ||
| } | ||
|
|
||
| _FOLLOWER_TOTAL_REFRESH_PLATFORMS: frozenset[str] = frozenset({"facebook", "instagram", "instagram_login"}) | ||
|
|
||
|
|
||
| def _needs_empty_follower_count_refresh(account) -> bool: | ||
| return account.follower_count <= 0 and account.platform in _FOLLOWER_TOTAL_REFRESH_PLATFORMS | ||
|
|
||
|
|
||
| def _sync_account_metrics(account, on_date: dt_date) -> None: | ||
| """Fetch account-level metrics for ``on_date`` and any recent missing days. | ||
|
|
@@ -415,7 +421,9 @@ def _sync_account_metrics(account, on_date: dt_date) -> None: | |
| current_followers = None | ||
| for offset in range(recent_days): | ||
| target = on_date - timedelta(days=offset) | ||
| if AccountInsightsSnapshot.objects.filter(social_account=account, date=target).exists(): | ||
| has_rows_for_day = AccountInsightsSnapshot.objects.filter(social_account=account, date=target).exists() | ||
| needs_current_follower_refresh = target == on_date and _needs_empty_follower_count_refresh(account) | ||
| if has_rows_for_day and not needs_current_follower_refresh: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When today's snapshot rows already exist but Useful? React with 👍 / 👎. |
||
| continue | ||
| start = datetime.combine(target, time.min, tzinfo=tz) | ||
| end = datetime.combine(target, time.max, tzinfo=tz) | ||
|
|
@@ -687,9 +695,9 @@ def sync_all_account_analytics() -> None: | |
| # backfill (see backfill_account_analytics), so the cron resumes on its | ||
| # own. The per-post Data-API loop below still runs (it uses the | ||
| # publish/read scopes the account already has). | ||
| if ( | ||
| not account.analytics_needs_reconnect | ||
| and not AccountInsightsSnapshot.objects.filter(social_account=account, date=today).exists() | ||
| has_today_rows = AccountInsightsSnapshot.objects.filter(social_account=account, date=today).exists() | ||
| if not account.analytics_needs_reconnect and ( | ||
| not has_today_rows or _needs_empty_follower_count_refresh(account) | ||
| ): | ||
| _sync_account_metrics(account, today) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| """Tests for analytics read-side services.""" | ||
|
|
||
| from datetime import timedelta | ||
|
|
||
| import pytest | ||
| from django.utils import timezone | ||
|
|
||
| from apps.social_accounts.models import SocialAccount | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def workspace(db, organization): | ||
| from apps.workspaces.models import Workspace | ||
|
|
||
| return Workspace.objects.create(name="Analytics Services WS", organization=organization) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def facebook_account(workspace): | ||
| return SocialAccount.objects.create( | ||
| workspace=workspace, | ||
| platform="facebook", | ||
| account_platform_id="page-1", | ||
| account_name="Facebook Page", | ||
| oauth_access_token="token", | ||
| connection_status=SocialAccount.ConnectionStatus.CONNECTED, | ||
| ) | ||
|
|
||
|
|
||
| def _published_platform_post(account): | ||
| from apps.composer.models import PlatformPost, Post | ||
|
|
||
| post = Post.objects.create(workspace=account.workspace, caption="hello") | ||
| return PlatformPost.objects.create( | ||
| post=post, | ||
| social_account=account, | ||
| status=PlatformPost.Status.PUBLISHED, | ||
| published_at=timezone.now(), | ||
| platform_post_id="post-1", | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| def test_account_bundle_prefers_fresher_post_fallback_for_content_metrics(facebook_account): | ||
| """Per-post Facebook analytics can refresh hourly while account snapshots are | ||
| daily. The main graph should use the fresher post-derived value instead of | ||
| leaving the overall insight chip/card stuck on the stale account row. | ||
| """ | ||
| from apps.analytics.models import AccountInsightsSnapshot, PostInsightsSnapshot | ||
| from apps.analytics.services import account_analytics_bundle | ||
|
|
||
| today = timezone.now().date() | ||
| old_capture = timezone.now() - timedelta(hours=2) | ||
| new_capture = timezone.now() | ||
| platform_post = _published_platform_post(facebook_account) | ||
|
|
||
| account_row = AccountInsightsSnapshot.objects.create( | ||
| social_account=facebook_account, | ||
| metric_key="views", | ||
| date=today, | ||
| value=10, | ||
| ) | ||
| AccountInsightsSnapshot.objects.filter(id=account_row.id).update(captured_at=old_capture) | ||
|
|
||
| post_row = PostInsightsSnapshot.objects.create( | ||
| platform_post=platform_post, | ||
| metric_key="views", | ||
| date=today, | ||
| value=42, | ||
| ) | ||
| PostInsightsSnapshot.objects.filter(id=post_row.id).update(captured_at=new_capture) | ||
|
|
||
| series = account_analytics_bundle(facebook_account, 7)["series_map"]["views"] | ||
|
|
||
| assert series[-1] == 42 | ||
|
|
||
|
|
||
| @pytest.mark.django_db | ||
| def test_account_bundle_keeps_account_reach_instead_of_summing_post_reach(facebook_account): | ||
| from apps.analytics.models import AccountInsightsSnapshot, PostInsightsSnapshot | ||
| from apps.analytics.services import account_analytics_bundle | ||
|
|
||
| today = timezone.now().date() | ||
| platform_post = _published_platform_post(facebook_account) | ||
|
|
||
| AccountInsightsSnapshot.objects.create( | ||
| social_account=facebook_account, | ||
| metric_key="reach", | ||
| date=today, | ||
| value=10, | ||
| ) | ||
| PostInsightsSnapshot.objects.create( | ||
| platform_post=platform_post, | ||
| metric_key="reach", | ||
| date=today, | ||
| value=42, | ||
| ) | ||
|
|
||
| series = account_analytics_bundle(facebook_account, 7)["series_map"]["reach"] | ||
|
|
||
| assert series[-1] == 10 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| from django.db import migrations | ||
|
|
||
|
|
||
| def normalize_facebook_platform_post_ids(apps, schema_editor): | ||
| PlatformPost = apps.get_model("composer", "PlatformPost") | ||
|
|
||
| queryset = PlatformPost.objects.filter( | ||
| social_account__platform="facebook", | ||
| platform_post_id__contains="_", | ||
| ).exclude(platform_post_id="") | ||
|
|
||
| for platform_post in queryset.iterator(): | ||
| platform_post.platform_post_id = str(platform_post.platform_post_id).rsplit("_", 1)[1] | ||
| platform_post.save(update_fields=["platform_post_id"]) | ||
|
|
||
|
|
||
| def noop_reverse(apps, schema_editor): | ||
| pass | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("composer", "0017_post_proposed_publish_at"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(normalize_facebook_platform_post_ids, noop_reverse), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the per-post fallback wins on freshness,
dailyis a sparse fallback series where omitted dates mean 0, butupdate()only overwrites dates present indaily. For a Facebook metric with older account rows across the window and fresher post snapshots only on one day, the chart will mix the fresher post value for that day with stale account values on every omitted day instead of using the selected post-derived series. Replace the metric's day map when fallback wins rather than merging it into the stale account map.Useful? React with 👍 / 👎.