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

1061 lines
18 KiB
Markdown

# 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=<secure-random-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.