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.
1061 lines
18 KiB
Markdown
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.
|