# Skinbase AdSense Analytics Module Status: Planned Project: Skinbase.org Integration: Google AdSense Management API v2 Authorization: OAuth 2.0 Web Server Flow Access level: Read-only --- # 1. Purpose Implement an internal AdSense analytics module inside the Skinbase administration area. The module must allow an administrator to: - connect the Skinbase application to a Google AdSense account using OAuth 2.0; - securely store long-lived authorization; - automatically synchronize AdSense statistics; - store historical statistics locally; - display AdSense performance inside the Skinbase admin; - compare performance of individual Skinbase ad placements; - inspect revenue, RPM, CTR, clicks, impressions and coverage; - inspect desktop/mobile/platform performance; - optionally inspect country-level performance; - manually trigger synchronization; - detect broken/revoked OAuth authorization and request reconnect. This is an internal administration feature. No AdSense credentials, statistics or management endpoints may be exposed to normal Skinbase users. --- # 2. Current Skinbase advertising strategy Skinbase currently displays ads only to anonymous / non-authenticated visitors. Registered users have an ad-free experience. Current known placement: - Artwork page - advertisement below Tags & Categories - advertisement before Comments Additional placements may exist or be introduced later, for example: - Homepage placement #1 - Homepage placement #2 - Browse/category pages - Search pages - News pages The analytics architecture must therefore support multiple ad units / placements without requiring schema changes. --- # 3. Google API Use Google AdSense Management API v2. Base API URL: ```text https://adsense.googleapis.com/v2 ``` OAuth authorization endpoint: ```text https://accounts.google.com/o/oauth2/v2/auth ``` OAuth token endpoint: ```text https://oauth2.googleapis.com/token ``` Required OAuth scope: ```text https://www.googleapis.com/auth/adsense.readonly ``` Do NOT request full AdSense management access. The module is analytics/read-only. --- # 4. OAuth configuration Production redirect URI: ```text https://skinbase.org/admin/adsense/oauth/callback ``` The redirect URI configured in Google Cloud must exactly match the redirect URI sent by Skinbase. Environment configuration: ```env ADSENSE_CLIENT_ID= ADSENSE_CLIENT_SECRET= ADSENSE_REDIRECT_URI=https://skinbase.org/admin/adsense/oauth/callback ADSENSE_SYNC_ENABLED=true ``` Do not commit real credentials. Add equivalent keys to `.env.example` without secrets. Create: ```text config/adsense.php ``` Suggested configuration: ```php return [ 'client_id' => env('ADSENSE_CLIENT_ID'), 'client_secret' => env('ADSENSE_CLIENT_SECRET'), 'redirect_uri' => env( 'ADSENSE_REDIRECT_URI', rtrim(env('APP_URL'), '/') . '/admin/adsense/oauth/callback' ), 'scope' => 'https://www.googleapis.com/auth/adsense.readonly', 'sync_enabled' => env('ADSENSE_SYNC_ENABLED', true), ]; ``` --- # 5. OAuth flow ```text Admin ↓ AdSense Analytics ↓ Connect Google AdSense ↓ Google OAuth ↓ Skinbase callback ↓ Token exchange ↓ Discover available AdSense accounts ↓ Select account if more than one ↓ Connection active ``` OAuth authorization request must use: ```text response_type=code access_type=offline include_granted_scopes=true scope=https://www.googleapis.com/auth/adsense.readonly state= ``` For an explicit new Connect/Reconnect operation use: ```text prompt=consent ``` Never use `prompt=consent` for normal token refreshes. --- # 6. OAuth security requirements OAuth implementation MUST: - generate a cryptographically secure `state`; - store the state in the authenticated admin session; - verify the callback state; - reject missing or invalid state; - reject callbacks containing OAuth errors; - exchange authorization code server-side; - never expose client secret to JavaScript; - never expose refresh/access tokens to JavaScript; - never log OAuth tokens; - encrypt refresh tokens at rest; - limit OAuth routes to authorized administrators; - use HTTPS in production. The refresh token is the important long-lived credential. Access tokens should preferably be cached and refreshed as required instead of being permanently stored in plain form. --- # 7. Important Google OAuth production note The Google Cloud OAuth application's publishing state must be considered. During development/testing, Google may issue refresh tokens with limited lifetime. Before relying on unattended production synchronization, verify that the OAuth application is configured appropriately for production. The admin page should detect an invalid or revoked refresh token and show: ```text AdSense authorization expired or was revoked. Reconnect Google AdSense. ``` --- # 8. Suggested database design ## 8.1 `adsense_connections` Suggested fields: ```text id account_resource_name account_display_name encrypted_refresh_token status connected_by_user_id connected_at last_sync_attempt_at last_successful_sync_at last_error created_at updated_at ``` Possible `status` values: ```text pending connected reconnect_required error ``` Use Laravel encryption / encrypted casts for the refresh token. ## 8.2 `adsense_entities` Suggested fields: ```text id account_resource_name ad_client_resource_name nullable type resource_name nullable reporting_dimension_id display_name active nullable last_seen_at created_at updated_at ``` Supported entity types initially: ```text ad_unit custom_channel ``` Use upsert semantics. ## 8.3 `adsense_daily_stats` Suggested fields: ```text id date account_resource_name dimension_type dimension_key dimension_label nullable currency_code page_views ad_requests matched_ad_requests impressions clicks estimated_earnings page_views_ctr page_views_rpm ad_requests_coverage ad_requests_ctr ad_requests_rpm impressions_ctr impressions_rpm cost_per_click last_synced_at created_at updated_at ``` Supported `dimension_type` values: ```text total ad_unit custom_channel platform country ``` For total rows: ```text dimension_type = total dimension_key = __total__ ``` Unique index: ```text account_resource_name date dimension_type dimension_key ``` Synchronization MUST be idempotent and use upsert/update semantics. --- # 9. AdSense entities synchronization After account connection and during regular synchronization: 1. Fetch AdSense accounts. 2. Use selected account. 3. Fetch ad clients. 4. For each relevant ad client: - fetch ad units; - fetch custom channels. 5. Store/update metadata in `adsense_entities`. API resources: ```text GET /v2/accounts GET /v2/{account}/adclients GET /v2/{account}/adclients/{adclient}/adunits GET /v2/{account}/adclients/{adclient}/customchannels ``` Implement pagination correctly. Do not assume one response page. --- # 10. Read-only inventory policy Do not implement creation, deletion or modification of AdSense: - ad units; - custom channels; - sites; - account settings. AdSense inventory configuration remains managed in Google AdSense. Skinbase only reads the data. --- # 11. Reporting API Generate reports using: ```text GET /v2/{account}/reports:generate ``` The parser must NOT assume fixed array positions. Parse returned rows using the report header names. Use header information to identify monetary metrics and their currency. --- # 12. Core metrics Synchronize these metrics where compatible: ```text ESTIMATED_EARNINGS PAGE_VIEWS PAGE_VIEWS_CTR PAGE_VIEWS_RPM AD_REQUESTS AD_REQUESTS_COVERAGE AD_REQUESTS_CTR AD_REQUESTS_RPM MATCHED_AD_REQUESTS IMPRESSIONS IMPRESSIONS_CTR IMPRESSIONS_RPM CLICKS COST_PER_CLICK ``` Do not manually derive Google metrics if the API already provides them. Ratios should be stored as decimals as returned by Google. --- # 13. Report dimensions ## Report A — Daily totals Dimensions: ```text DATE ``` ## Report B — Ad units Dimensions: ```text DATE AD_UNIT_ID ``` Map `AD_UNIT_ID` to the local entity display name. Recommended placement names: ```text Skinbase_Artwork_BeforeComments Skinbase_Home_AfterTrending Skinbase_Home_AfterFresh Skinbase_Browse_InFeed Skinbase_Search_InFeed Skinbase_News_InArticle ``` ## Report C — Custom channels Dimensions: ```text DATE CUSTOM_CHANNEL_ID ``` ## Report D — Platform Dimensions: ```text DATE PLATFORM_TYPE_CODE ``` ## Report E — Country Dimensions: ```text DATE COUNTRY_CODE ``` Country statistics are secondary and must not block successful core synchronization. --- # 14. Partial failure behavior Each report must be isolated. Example: ```text total succeeded ad_unit succeeded platform succeeded country failed ``` The first three successful datasets must still be saved. Do not roll back all reporting because one optional report failed. --- # 15. Suggested service architecture ```text App\Services\Adsense\AdsenseOAuthService App\Services\Adsense\AdsenseApiClient App\Services\Adsense\AdsenseReportParser App\Services\Adsense\AdsenseSyncService ``` Adapt namespaces and class names to the existing Skinbase conventions. ## `AdsenseOAuthService` - OAuth authorization URL; - authorization-code exchange; - refresh access token; - refresh-token lifecycle. ## `AdsenseApiClient` - authenticated requests; - pagination; - retries; - response validation. ## `AdsenseReportParser` - parse headers by name; - map cells; - detect currency; - normalize integer/decimal values. ## `AdsenseSyncService` - synchronize metadata; - synchronize reports; - persist normalized rows; - update connection status; - isolate partial failures. --- # 16. Token handling Normal flow: ```text stored encrypted refresh token ↓ request access token ↓ cache short-lived access token ↓ call AdSense API ``` If an API call fails due to an expired token: ```text refresh access token retry request once ``` If refresh fails permanently: ```text connection.status = reconnect_required ``` Never loop indefinitely. --- # 17. Artisan command Create: ```bash php artisan adsense:sync ``` Options: ```bash 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 ``` Optional: ```bash php artisan adsense:sync --force ``` Never print tokens. --- # 18. Scheduler Use the existing Laravel scheduling architecture. Suggested schedule: - hourly: synchronize previous 3 days; - nightly: synchronize previous 30 days. Use `withoutOverlapping()`. If the existing deployment uses distributed scheduler locking, follow its existing `onOneServer()` convention. --- # 19. Manual sync Admin dashboard must provide: ```text Sync now ``` The action must use admin authorization and CSRF protection. For large historical syncs, use the project's existing queue architecture rather than blocking the HTTP request. --- # 20. Admin routes Suggested routes: ```text GET /admin/adsense GET /admin/adsense/connect GET /admin/adsense/oauth/callback POST /admin/adsense/account POST /admin/adsense/sync DELETE /admin/adsense/disconnect ``` Use existing admin middleware and authorization. --- # 21. Admin dashboard Create a page consistent with the current Skinbase admin design. Connection card: ```text Google AdSense Connected Account: Skinbase / pub-... Last sync: 12 minutes ago [ Sync now ] [ Reconnect ] [ Disconnect ] ``` Support statuses: ```text Not connected Connected Synchronization error Reconnect required ``` --- # 22. Period selector Support: ```text Today Yesterday Last 7 days Last 30 days Custom range ``` Default: ```text Last 30 days ``` --- # 23. KPI cards Show: ```text Estimated earnings Page views Ad impressions Clicks Page CTR Page RPM Ad request coverage CPC ``` Where useful, compare with the immediately previous equal-length period. Never divide by zero. --- # 24. Revenue chart Primary chart: ```text Estimated earnings by day ``` Reuse the chart library already available in Skinbase. Optional toggles: ```text Revenue Page views Page RPM Clicks ``` --- # 25. Placement performance table Primary table: ```text Ad placement Views Impressions Clicks CTR RPM Revenue ------------------------------------------------------------------------------------ Artwork Before Comments ... ... ... ... ... ... Homepage Trending ... ... ... ... ... ... Homepage Fresh ... ... ... ... ... ... ``` Default sort: ```text Revenue descending ``` This is a key part of the module because Skinbase must be able to evaluate individual ad placements. --- # 26. Platform breakdown Show performance by platform: ```text Desktop Mobile Tablet ``` Metrics: ```text Page views Impressions Clicks RPM Revenue ``` --- # 27. Country breakdown Optional table: ```text Country Page views Impressions Clicks RPM Revenue ``` Default: ```text Top 20 countries by revenue ``` --- # 28. Data freshness Display the last synchronization time and show: ```text Today's AdSense revenue is estimated and may change. ``` Do not imply that current-day values are final. --- # 29. Disconnect behavior Disconnect must: 1. require authenticated admin; 2. use POST/DELETE + CSRF; 3. remove local refresh credentials; 4. clear cached access token; 5. retain historical statistics. Historical reporting data must NOT be deleted automatically. Optional: best-effort Google token revocation. --- # 30. Error handling Handle at minimum: ```text OAuth callback denied invalid state missing authorization code invalid_grant expired/revoked refresh token AdSense API 401 AdSense API 403 AdSense API 429 Google 5xx network timeout no AdSense accounts multiple AdSense accounts invalid report combination malformed report response ``` Use bounded retry/backoff for transient errors. --- # 31. Logging Never log: ```text client_secret access_token refresh_token authorization code ``` Use sanitized logging only. --- # 32. Testing Implement automated tests covering at least: ## OAuth - admin can start OAuth; - non-admin cannot; - correct readonly scope; - offline access; - secure state generation; - valid callback; - invalid state; - OAuth errors; - mocked token exchange; - refresh token encrypted at rest. ## API - access-token refresh; - accounts list; - pagination; - ad units; - custom channels; - 401 handling; - 429 retry; - revoked token -> reconnect required. ## Reporting - headers mapped by name; - parser independent from column order; - currency extraction; - total report; - ad-unit report; - platform report; - country report; - decimal ratio handling; - idempotent upsert; - repeat sync updates existing rows. ## Admin - dashboard admin-only; - connection states; - period filters; - placement aggregation; - manual sync authorization. All Google traffic must be mocked in tests. --- # 33. Privacy and security This module imports aggregated advertising performance data only. Do not store visitor-level Google advertising data, IP addresses, Google user profiles or visitor identifiers. Do not add new public tracking as part of this module. --- # 34. Public-site performance requirement No public Skinbase page may call the AdSense Management API. Management API calls are only allowed from: ```text OAuth flow admin manual sync scheduled synchronization ``` The admin dashboard reads statistics from the local database. Existing advertising behavior remains: - registered users: ad-free; - anonymous visitors: ads may be displayed. --- # 35. Initial synchronization After first successful OAuth connection: 1. synchronize entity metadata; 2. synchronize previous 30 days; 3. mark connection successful; 4. redirect to the AdSense dashboard. Do not automatically import an unbounded history. Longer history can later be imported manually: ```bash php artisan adsense:sync --from=2026-01-01 --to=2026-09-20 ``` --- # 36. Google Cloud setup documentation Document these manual steps: 1. Create/select Google Cloud project. 2. Enable Google AdSense Management API. 3. Configure OAuth consent screen. 4. Create OAuth Client ID of type Web application. 5. Add exact redirect URI: ```text https://skinbase.org/admin/adsense/oauth/callback ``` 6. Configure environment variables. 7. Deploy. 8. Open Skinbase admin. 9. Click Connect Google AdSense. 10. Grant access. 11. Verify account. 12. Verify first synchronization. Before unattended production sync is relied on, review the OAuth application's production/publishing configuration. --- # 37. Definition of Done The module is complete when: - an administrator can connect Google AdSense; - OAuth uses `adsense.readonly`; - offline access / refresh token works; - refresh token is encrypted; - available AdSense accounts can be discovered and selected; - ad units are discovered; - custom channels are discovered; - daily reports synchronize; - statistics are persisted locally; - repeated synchronization is idempotent; - scheduler refreshes recent data; - manual synchronization works; - dashboard displays revenue and performance; - dashboard displays placement breakdown; - dashboard displays platform breakdown; - invalid authorization produces reconnect state; - credentials never appear in logs; - tests pass; - `.env.example` is updated; - Google Cloud setup is documented; - no public Skinbase page depends on the management API. --- # 38. Future extension — not part of initial implementation A later milestone may combine AdSense aggregates with Skinbase-owned analytics such as: ```text anonymous page views registrations downloads artwork views guest -> registration conversion ad revenue ``` Possible future metrics: ```text Ad revenue / 1,000 anonymous Skinbase visits Revenue per artwork page Guest registration rate before/after advertising Revenue impact by page type ``` Do not scope-creep this into the first implementation.