# Game tracking and privacy integration

> **This is the small download with media placeholders.** Before uploading, put each original media file beside its `INSERT_<filename>.txt` placeholder, then delete the TXT placeholder. Follow `READ-ME-FIRST.txt`. Website code and media URLs are unchanged.

Updated 22 October 2026. This package includes **Bubble Pop**, **Crystal Guard**, and **Spooder Solitaire** at their existing directory paths on **https://yunadata.pages.dev/**.

## What changed

- Each game uses the shared website consent preference, with Cookie settings in its footer.
- The unconditional GTM loader and GTM noscript iframe were removed. The consent code owns GTM loading.
- The existing 13 gameplay event calls now pass through `game-privacy.js`. Their names, parameters, values, order, and locations in the game logic are preserved.
- Bubble Pop and Crystal Guard’s existing Firebase Analytics connections load only after analytics opt-in. Their Firebase projects, SDK versions, and measurement IDs are retained. Spooder Solitaire did not initialize Firebase Analytics in the supplied code, so none was added.
- Footer privacy links open the local policy in a new tab, keeping the current game open. The home logo is a native keyboard-accessible link. The homepage’s game covers now open the migrated local game directories.
- The privacy policy now explains public leaderboard records separately from optional analytics.

Each original game stylesheet is preserved apart from its background-image URL, which now uses the bundled original image. Bubble Pop’s canvas watermark and Spooder’s text watermark display `yunadata.pages.dev`. Input handling, scoring, difficulty, timers, randomness, leaderboard queries, and leaderboard writes are unchanged. New CSS is scoped to consent controls, footer links, and the navigation logo; it does not import the portfolio’s theme into the games.

## Files to install together

Use the complete `yunadata-cloudflare-placeholders.zip` as a Cloudflare Pages Direct Upload. It includes TXT placeholders for the original images and videos; insert the actual media before uploading and keep all included directories together. See [CLOUDFLARE-SETUP.md](CLOUDFLARE-SETUP.md). It includes all three game directories, plus the shared files at the website root:

- `consent.js`, `consent-ui.js`, `consent.css`
- `game-privacy.js`, `game-privacy.css`
- Updated `index.html` and `privacy-policy.html`

Do not upload just `game.js`: the games now use the shared adapter and consent controls. Insert the original background images locally at their placeholder locations. Fonts and Firebase endpoints retain their original service URLs. No build step is needed.

The private preview disables production analytics by hostname. Its games still use their original Firebase leaderboard services, so a real score submission there would write to the existing leaderboard. Automated verification used mocked database calls and did not write scores to your Firebase projects.

## Existing event contract

These are the fields in the supplied game code, retained exactly:

| Game | Event | Original parameters |
| --- | --- | --- |
| Bubble Pop | `game_start` | `game_name` |
| Bubble Pop | `level_up` | `game_name`, `level_number` |
| Bubble Pop | `game_complete` | `game_name`, `game_score` |
| Bubble Pop | `score_submission` | `game_name`, `game_score` |
| Crystal Guard | `game_start` | `game_name` |
| Crystal Guard | `tower_upgrade` | `tower_type` |
| Crystal Guard | `wave_complete` | `wave_number` |
| Crystal Guard | `game_complete` | `game_name`, `game_score` |
| Crystal Guard | `score_submission` | `game_name`, `score` |
| Spooder Solitaire | `game_start` | `game_name`, `difficulty` |
| Spooder Solitaire | `level_up` | `game_name`, `level_number` |
| Spooder Solitaire | `game_complete` | `game_name`, `game_score`, `difficulty` |
| Spooder Solitaire | `score_submission` | `game_name`, `game_score`, `difficulty` |

Crystal Guard deliberately retains `score` on its submission event, while the other two use `game_score`. Check your existing GTM mapping rather than renaming a field and disrupting historical reports. Its tower/wave events originally inherit `game_name` from the data layer. The adapter supplies the constant `Crystal Guard` context after consent if the visitor accepted after starting a game; it does not fabricate a missed `game_start` event or alter either event object.

Play before opt-in is not recorded or replayed. If the player has already opted in but GTM is still loading, up to 100 game events wait in page memory and are delivered in order once GTM is ready. This preserves a start event made immediately after accepting. Withdrawal clears that queue. Nothing is written to browser storage for this event queue, and a blocked loader never blocks play.

## GTM and Firebase setup

Follow the replacement [GTM-MIGRATION.md](GTM-MIGRATION.md) from the current, unchanged GTM container. It creates separate GitHub and Cloudflare branches, preserving the old GitHub site during the gradual move. The new consent guard works only on `yunadata.pages.dev`, including these three game pages.

In the **Cloudflare copies** of the gameplay tags, keep the existing event names and conditions. Add **Page Hostname equals yunadata.pages.dev** and **JS - Yuna analytics allowed equals true** to every applicable firing trigger, and require `analytics_storage` in that copy’s consent settings. Keep the legacy GitHub tags scoped to GitHub with their original consent behavior. Preserve existing game parameter mappings. Do not replace the game event triggers with `yd_analytics_ready`; that event is for initializing the Google tag and Clarity once per page.

| Game | Existing Firebase Analytics measurement ID | Ownership after migration |
| --- | --- | --- |
| Bubble Pop | `G-CH120T2QKY` | Existing Firebase SDK, now loaded after consent |
| Crystal Guard | `G-4PZEYHZLYW` | Existing Firebase SDK, now loaded after consent |
| Spooder Solitaire | `G-9GFS4T2KQG` appears in the Firebase config | Config retained; no Firebase Analytics initializer was present, and none was added |

Do not add duplicate initialization tags in GTM for the two IDs already initialized by Firebase. If GTM already initializes the same ID, check for duplicate page views and keep one initializer for that destination; leave your existing gameplay event routing intact. Separate configured measurement IDs can legitimately receive their own initial page view. Your actual GTM destinations were not included in the attachments, so the container configuration still needs checking in Tag Assistant.

Firebase consent is explicitly set before Analytics initialization, with analytics granted and all advertising consent denied. On withdrawal, its measurement ID is disabled immediately, and the shared consent system refreshes the page to unload running trackers. **Withdrawing analytics during a game restarts that game.** The settings panel warns about this before saving. Declining when analytics has never loaded does not restart the game.

**Firestore leaderboards remain independent of analytics consent.** Declining tracking does not prevent reading or submitting scores. The supplied Firebase rules and database schemas are unchanged, and no Firebase or GTM account changes were made. No rule edit is required solely for this consent migration. The extra Blobble Hop rules in the attachment are outside these three games and were not used.

The existing rules permit public access without authenticating player ownership. This migration preserves that behavior; it is not a redesign of leaderboard security or an anti-cheat system.

## Verification

Source comparisons check the original CSS and gameplay code while allowing only the named analytics substitutions, local background URLs, and the two domain watermarks. The original game markup, inline Spooder Firestore code, and all 13 event payloads are checked against captured originals. Canonicals and structured-data URLs now point to Cloudflare. The public Firebase configuration and leaderboard data shapes are retained.

Offline tests cover declined consent, late opt-in, GTM startup ordering, queue clearing on withdrawal, tracking exceptions, blocked/unsupported Firebase Analytics, opt-out during asynchronous SDK loading, immediate collection disabling, and all 13 event objects. Mocked score submissions for all three games succeed while analytics is declined. No live leaderboard records were created or changed.

The managed browser preview service was unavailable. Real rendering, mobile play, touch/drag behavior, live Firebase access, and live GTM delivery could not be exercised here. Before public cutover, check:

1. Start a game with analytics rejected: play and leaderboard operations should work, with no GTM/GA/Clarity or Firebase Analytics initialization.
2. Accept analytics, then start a fresh game: inspect the existing event names and parameter values in Tag Assistant/GA4 DebugView, including the Crystal Guard `score` field.
3. Reject then opt in during a game: only subsequent actions should be tracked; Crystal Guard wave/upgrade events should retain game context.
4. Open privacy from the footer: the game stays open in its original tab. Reopen Cookie settings and verify the saved choice.
5. Test analytics withdrawal after finishing a round, since it refreshes the page.
6. Check each game on desktop and phone, and check that the same Firebase leaderboard data and existing high scores appear. Avoid submitting synthetic test entries to the live leaderboard.

Official reference: [Firebase Web Analytics APIs](https://firebase.google.com/docs/reference/js/analytics), including `setConsent`, `getAnalytics`, `isSupported`, and `setAnalyticsCollectionEnabled`.
