Files
SkinbaseNova/docs/SKINBASE_ADSENSE_ANALYTICS.md
T
test 4d60a876af 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

18 KiB

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:

https://adsense.googleapis.com/v2

OAuth authorization endpoint:

https://accounts.google.com/o/oauth2/v2/auth

OAuth token endpoint:

https://oauth2.googleapis.com/token

Required OAuth scope:

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:

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:

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:

config/adsense.php

Suggested configuration:

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

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:

response_type=code
access_type=offline
include_granted_scopes=true
scope=https://www.googleapis.com/auth/adsense.readonly
state=<secure-random-state>

For an explicit new Connect/Reconnect operation use:

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:

AdSense authorization expired or was revoked.
Reconnect Google AdSense.

8. Suggested database design

8.1 adsense_connections

Suggested fields:

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:

pending
connected
reconnect_required
error

Use Laravel encryption / encrypted casts for the refresh token.

8.2 adsense_entities

Suggested fields:

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:

ad_unit
custom_channel

Use upsert semantics.

8.3 adsense_daily_stats

Suggested fields:

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:

total
ad_unit
custom_channel
platform
country

For total rows:

dimension_type = total
dimension_key  = __total__

Unique index:

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:

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:

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:

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:

DATE

Report B — Ad units

Dimensions:

DATE
AD_UNIT_ID

Map AD_UNIT_ID to the local entity display name.

Recommended placement names:

Skinbase_Artwork_BeforeComments
Skinbase_Home_AfterTrending
Skinbase_Home_AfterFresh
Skinbase_Browse_InFeed
Skinbase_Search_InFeed
Skinbase_News_InArticle

Report C — Custom channels

Dimensions:

DATE
CUSTOM_CHANNEL_ID

Report D — Platform

Dimensions:

DATE
PLATFORM_TYPE_CODE

Report E — Country

Dimensions:

DATE
COUNTRY_CODE

Country statistics are secondary and must not block successful core synchronization.


14. Partial failure behavior

Each report must be isolated.

Example:

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

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:

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:

refresh access token
retry request once

If refresh fails permanently:

connection.status = reconnect_required

Never loop indefinitely.


17. Artisan command

Create:

php artisan adsense:sync

Options:

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:

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:

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:

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:

Google AdSense
Connected

Account: Skinbase / pub-...
Last sync: 12 minutes ago

[ Sync now ]
[ Reconnect ]
[ Disconnect ]

Support statuses:

Not connected
Connected
Synchronization error
Reconnect required

22. Period selector

Support:

Today
Yesterday
Last 7 days
Last 30 days
Custom range

Default:

Last 30 days

23. KPI cards

Show:

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:

Estimated earnings by day

Reuse the chart library already available in Skinbase.

Optional toggles:

Revenue
Page views
Page RPM
Clicks

25. Placement performance table

Primary table:

Ad placement                   Views   Impressions   Clicks   CTR    RPM     Revenue
------------------------------------------------------------------------------------
Artwork Before Comments        ...     ...           ...      ...    ...     ...
Homepage Trending              ...     ...           ...      ...    ...     ...
Homepage Fresh                 ...     ...           ...      ...    ...     ...

Default sort:

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:

Desktop
Mobile
Tablet

Metrics:

Page views
Impressions
Clicks
RPM
Revenue

27. Country breakdown

Optional table:

Country
Page views
Impressions
Clicks
RPM
Revenue

Default:

Top 20 countries by revenue

28. Data freshness

Display the last synchronization time and show:

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:

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:

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:

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:

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:
https://skinbase.org/admin/adsense/oauth/callback
  1. Configure environment variables.
  2. Deploy.
  3. Open Skinbase admin.
  4. Click Connect Google AdSense.
  5. Grant access.
  6. Verify account.
  7. 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:

anonymous page views
registrations
downloads
artwork views
guest -> registration conversion
ad revenue

Possible future metrics:

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.