Files
SkinbaseNova/docs/ADSENSE_ANALYTICS.md
T
klevze ce0f278bac Add a read-only Google AdSense analytics module for admins.
Connect AdSense over OAuth, sync entities and daily reports locally, and show placement performance in the admin panel without calling the Management API from public pages.
2026-09-20 14:49:48 +02:00

7.5 KiB

Skinbase AdSense Analytics Module

Status: Implemented
Project: Skinbase.org
Integration: Google AdSense Management API v2
Authorization: OAuth 2.0 Web Server Flow
Access level: Read-only (adsense.readonly)

This document is the implementation and operations guide for the admin AdSense analytics module. Public advertising behavior is unchanged: registered users remain ad-free; anonymous visitors may see AdSense units.


1. What the module does

Administrators can:

  • connect Skinbase to Google AdSense with OAuth 2.0 (offline / refresh token);
  • discover AdSense accounts, ad clients, ad units and custom channels;
  • synchronize daily reporting into local MySQL tables;
  • view an admin dashboard with totals, placement comparison, platform and country breakdowns;
  • run hourly/nightly scheduled syncs or a manual Sync now;
  • reconnect after Google revokes authorization.

No public page calls the AdSense Management API. The dashboard reads only local tables.


2. Google Cloud setup

  1. Create or select a Google Cloud project.
  2. Enable Google AdSense Management API.
  3. Configure the OAuth consent screen (Internal or production publishing as required).
  4. Create an OAuth Client ID of type Web application.
  5. Add this exact authorized redirect URI:
https://skinbase.org/admin/adsense/oauth/callback
  1. Copy the client ID and client secret into the Skinbase environment (never into git).
  2. Review Google OAuth publishing / testing limits before relying on unattended production refresh tokens.

3. Environment

Add to the deployment environment (see .env.example):

ADSENSE_CLIENT_ID=
ADSENSE_CLIENT_SECRET=
ADSENSE_REDIRECT_URI=https://skinbase.org/admin/adsense/oauth/callback
ADSENSE_SYNC_ENABLED=true

Configuration lives in config/adsense.php. Do not store the Google client secret in the database.


4. Deploy commands

php artisan migrate --force
php artisan config:clear
php artisan route:clear
php artisan view:clear

Scheduler already loads routes/console.php. Ensure the production scheduler / cron is running (php artisan schedule:run every minute). Horizon / Redis queues must be running so SyncAdsenseJob can process manual and post-OAuth synchronizations.


5. Connect from Skinbase admin

  1. Sign in as an admin (not manager/editorial).
  2. Open AdSense Analytics (/admin/adsense) from the admin sidebar.
  3. Click Connect Google AdSense.
  4. Approve read-only AdSense access.
  5. If multiple AdSense accounts exist, select one.
  6. Initial sync imports entities and the previous 30 days.
  7. Confirm the dashboard shows the account and last synchronization time.

Reconnect uses prompt=consent so Google issues a new refresh token. Token refresh never sends prompt=consent.


6. Artisan

php artisan adsense:sync
php artisan adsense:sync --days=3
php artisan adsense:sync --days=30
php artisan adsense:sync --from=2026-09-01 --to=2026-09-20
php artisan adsense:sync --entities-only
php artisan adsense:sync --force

--force is required when ADSENSE_SYNC_ENABLED=false. --days cannot be combined with --from/--to.

The command never prints tokens.


7. Scheduler

Registered in routes/console.php:

Cadence Command Why
Hourly at :08 adsense:sync --days=3 Recent totals, including today, can still change
Nightly at 04:35 adsense:sync --days=30 Late adjustments to the last month

Both use withoutOverlapping() and runInBackground(). The project does not use onOneServer().


8. Schema

adsense_connections

Encrypted refresh token (encrypted Eloquent cast), status (pending, connected, reconnect_required, error, disconnected), selected account, sync timestamps, last error. Access tokens are cached, not stored permanently.

adsense_entities

Upserted metadata for ad_unit and custom_channel. Entities are not deleted if they disappear from a single API page.

Inventory sync uses AdSense for Content (AFC / ca-pub-...) clients. YouTube host clients (ca-yt-host-pub-...) are listed by Google but do not expose ad units; they are skipped. A 404 on a single ad client does not fail the rest of the synchronization.

adsense_daily_stats

Idempotent daily rows keyed by account_resource_name + date + dimension_type + dimension_key.

Dimension types: total, ad_unit, custom_channel, platform, country.

Monetary columns are DECIMAL, not floats.

Disconnect removes credentials and cached access tokens. Historical statistics are retained.


9. Reports

Separate report definitions against GET /v2/{account}/reports:generate:

Report Dimensions
A total DATE
B ad unit DATE, AD_UNIT_ID
C custom channel DATE, CUSTOM_CHANNEL_ID
D platform DATE, PLATFORM_TYPE_CODE
E country DATE, COUNTRY_CODE

The parser maps cells by header name, not position, and reads currency from METRIC_CURRENCY headers.

Country (and any other isolated report) can fail without rolling back successful reports.


10. Public advertising (unchanged)

  • Guests may see AdSense (AdSenseUnit / <x-ad-unit> / homepage ins.adsbygoogle).
  • Authenticated users do not receive those units.
  • Artwork placement remains below Tags & Categories and before Comments.

Recommended Google AdSense unit names (rename inside Google AdSense; Skinbase is read-only):

Skinbase_Artwork_BeforeComments
Skinbase_Home_AfterTrending
Skinbase_Home_AfterFresh
Skinbase_Browse_InFeed
Skinbase_Search_InFeed
Skinbase_News_InArticle

Current known slots:

Surface Slot Recommended name
Artwork before comments 4398418697 Skinbase_Artwork_BeforeComments
Homepage after trending (guest) 8766048277 Skinbase_Home_AfterTrending

11. Security

  • OAuth routes: auth + admin.role.
  • Cryptographic state stored in the admin session and verified on callback.
  • Authorization code exchange is server-side only.
  • Refresh tokens encrypted at rest; access tokens cached.
  • Tokens, authorization codes and client secrets are never sent to Inertia/JS and must not be logged.
  • Scope is only https://www.googleapis.com/auth/adsense.readonly.
  • Production redirect URI is HTTPS: https://skinbase.org/admin/adsense/oauth/callback.

12. Architecture

App\Services\Adsense\AdsenseOAuthService
App\Services\Adsense\AdsenseApiClient
App\Services\Adsense\AdsenseReportParser
App\Services\Adsense\AdsenseSyncService
App\Services\Adsense\AdsenseAnalyticsQueryService
App\Http\Controllers\Admin\AdsenseAnalyticsController
App\Jobs\SyncAdsenseJob
App\Console\Commands\AdsenseSyncCommand

HTTP uses Laravel's HTTP client. The Google PHP SDK is not required.


13. Routes

Method Path Name
GET /admin/adsense admin.adsense.index
GET /admin/adsense/connect admin.adsense.connect
GET /admin/adsense/oauth/callback admin.adsense.callback
POST /admin/adsense/account admin.adsense.account
POST /admin/adsense/sync admin.adsense.sync
DELETE /admin/adsense/disconnect admin.adsense.disconnect

These are registered before the legacy /admin/{path} → /moderation/{path} redirect so the Google redirect URI stays exact.


14. Tests

php artisan test --testsuite=Unit --filter=Adsense
php artisan test tests/Feature/Adsense

All Google HTTP is faked. Tests never require live Google credentials.