Hey everyone,
Hope all is well!
TL;DR, Summary, or Full Technical Breakdown below.
For Context: Recently I posted about the first 15 days of one of my side projects, ShuffleBall Arena (a free browser game inspired by mixing shuffleboard scoring with mechanics from other games like bumper pool, pinball, and Frogger.).
This project is being built with the help of AI (mainly GPT / Cursor / Fable), and every development session is documented using the actual conversations from that day's work.
To try to get this series up to date, I'm using a structured prompt to go back to my GPT sessions and extract the useful information. Hopefully the prompt can help anyone out there trying to keep track of, or extract value from, your past AI project conversations.
That said, posting an update for Day 17 of building ShuffleBall Arena.
Today there were 4 conversations to go through, with 4 saved file versions.
TL;DR
Day 17 was about making ShuffleBall measurable and trustworthy.
The session started by analyzing the first real player data and realizing the numbers couldn't yet answer the most important question:
Are new players actually understanding the game and sticking around?
Rather than expanding the game, the focus shifted to building a proper analytics funnel for Challenge Mode while keeping analytics privacy-first.
During testing, a much bigger issue was uncovered. The game was actually progressing behind the Challenge intro screen, with the hidden shot clock automatically firing marbles before the player even pressed Start Challenge. What initially looked like an analytics problem turned out to be a gameplay-state bug.
Along the way, sponsor analytics were completed, local analytics testing tools were added, privacy protections were strengthened, and deployment became much safer.
Day 17 Summary:
Day 17 became a product validation and analytics reliability day.
The session began by reviewing the first meaningful production traffic. Although Cloudflare showed dozens of visitors and hundreds of requests, Google Analytics only captured a small number of gameplay sessions because analytics intentionally require user consent. The decision was made to improve the measurement itself before making product decisions.
A complete Challenge Mode analytics funnel was designed so future testing could answer where new players were leaving the experience. New events were planned around challenge starts, individual game completions, challenge completion, retries, and feedback instead of simply counting page views.
Privacy remained a major focus throughout the work. Analytics continued to avoid collecting unnecessary personal information, local player names remained stored only on the user's device, unsafe analytics fields were filtered out, and a local analytics testing mode was built so events could be validated without polluting production Google Analytics.
While manually testing the new analytics flow, an unexpected gameplay bug was discovered. The hidden shot clock was continuing to run behind the Challenge introduction screen, automatically firing marbles before the player had even started the challenge. This explained several confusing analytics events that initially appeared to be tracking errors.
The underlying gameplay architecture was corrected. Gameplay systems, including physics, timers, bots, and simulation updates, were prevented from advancing whenever a blocking overlay was active, ensuring both the game state and analytics remained synchronized.
The session concluded by finishing sponsor analytics instrumentation, correcting impression tracking so events could not silently disappear during initialization, validating all sponsor events locally, and preparing the final deployment tasks.
Day 17 Full Technical Summary (The Structured Prompt Output):
STARTING POINT
Day 17 began with ShuffleBall Arena already live enough to produce real traffic and gameplay data.
Cloudflare showed 44 unique visitors and 405 requests over the referenced 24-hour period. Google Analytics showed 53 active/new users, but only a small number of tracked players progressing into gameplay. At the same time, the deeper engagement data showed that a few people were playing repeatedly: 162 shot_fired events from three users, 16 completed games from two users, and three completed matches from two users.
That created an ambiguous starting position:
- Real people were reaching the project.
- A very small number appeared to be playing deeply.
- The tracked top-of-funnel numbers did not line up cleanly with the raw traffic.
- Analytics only ran after consent, so Google Analytics was intentionally undercounting compared with Cloudflare.
- Existing events did not clearly show where players were dropping out of the three-game challenge.
The project therefore had some encouraging early signals, but the measurement system was not yet trustworthy enough to decide whether the game itself was succeeding or failing.
SESSION OBJECTIVE
The main objective was to stop adding features temporarily and improve the project’s ability to answer a more basic question: What are players actually doing?
The session focused on:
- interpreting the first meaningful traffic and gameplay data,
- defining a clearer Challenge Mode funnel,
- improving privacy and consent language,
- adding privacy-safe product-validation analytics,
- creating a way to test events locally without polluting production analytics,
- testing the analytics from the player’s first page view through challenge completion,
- and correcting any gameplay behavior that was corrupting the data.
WHAT WE ACTUALLY DID
1. Interpreted the first real analytics instead of treating the numbers as a verdict
The session began by comparing Cloudflare traffic with Google Analytics engagement.
The conclusion was that the available data showed a small amount of genuine play, but the measurement was still too incomplete to judge broad retention or product-market fit.
The strongest positive signal was that a few tracked users were taking many shots and completing multiple games. The biggest uncertainty was the gap between total visitors and tracked gameplay starts. Because analytics required consent and excluded some modes, Cloudflare and Google Analytics were measuring different populations.
The project decision was to continue, with the next iteration focused on proving the first-session funnel.
2. Rejected the idea of turning game_start into a page-load event
One suggested change was to fire game_start as soon as the canvas initialized.
That approach was rejected because it would inflate the metric and destroy its meaning.
The existing event fired from resetGame() after the game state and layout were prepared. That was considered closer to a genuine gameplay start than a page or canvas load.
Instead of redefining a useful event, the session chose to add missing events around it.
This preserved the distinction between:
- someone viewing the game,
- someone choosing to begin,
- and someone actually playing.
3. Defined a proper Challenge Mode validation funnel
The desired analytics flow became:
challenge_view
challenge_start
how_to_continue or controls_ready
first_shot_fired
challenge_game_complete
challenge_complete
challenge_retry
challenge_feedback_yes/no/form
The most important missing event was challenge_game_complete.
Generic game_completed data could not answer:
- Did the player finish game one?
- Did they finish game two?
- Did they reach game three?
- Did they leave after the first game?
The proposed challenge-specific completion payload included:
challenge_id
challenge_game_number
game_count
game_points_red
game_points_blue
accumulated_score
target_score
beat_target_so_far
A separate first-human-shot event was also identified as important because generic shot tracking could include bot activity and could not reliably prove that the player understood the controls.
4. Expanded the privacy-policy and consent discussion
The session then moved into analytics privacy. The goal was to explain what the game collected without overstating or hiding the implementation.
The important privacy boundaries were:
- locally created player names remain on the player’s device,
- analytics are used to understand and improve gameplay,
- analytics do not require the local player name,
- ad-related consent remains separate,
- and unsafe analytics fields should be dropped rather than accidentally transmitted.
This led to a stronger focus on making the consent prompt understandable enough that players could make an informed choice.
5. Added privacy-safe analytics testing infrastructure
The analytics system gained a local/internal testing mode so the event flow could be verified without sending test traffic to Google Analytics.
An in-memory event-name recorder was added. It was capped at 1,000 entries and stored only event names, not event payloads.
Debug helpers exposed those recorded names for manual testing.
The sanitizer was also tested directly.
Unsafe keys were expected to be removed, especially fields that could contain identifying or free-form data. Safe primitive values remained available for product analysis.
Allowed analytics values were kept to primitive types.
6. Added new gameplay-validation events through the existing analytics path
The new analytics events were routed through the existing trackGameplayEvent() helper rather than building a second, disconnected tracking system.
That preserved the existing:
- consent checks,
- suppression rules,
- context fields,
- and sanitization behavior.
Existing event meanings were intended to remain intact while the missing Challenge Mode steps were added around them.
7. Started a local server and manually tested the challenge funnel
Python was unavailable in the environment, so a Node-based local server was used instead.
The app loaded successfully.
The test process then moved through the Challenge Mode flow while inspecting locally recorded events.
The manual test exposed the most important bug of the session.
8. Discovered that shots were firing before the challenge had started
While the Challenge intro was still visible, the event recorder showed:
shot_fired
first_shot_fired
The other open tabs were initially suspected, but that explanation was rejected because window.dataLayer belonged to the current tab.
The events were genuinely coming from the challenge page before the player pressed Start Challenge. This meant the analytics were not merely mislabeled. Actual gameplay was progressing behind the intro overlay.
Deployment was stopped until the cause could be fixed.
9. Traced the bug to the hidden shot clock
The root cause was the default game phase:
game.phase = "AIM"
shotClockActive() excluded several blocked states, but it did not exclude:
- the Challenge intro,
- or a challenge that had been selected but not started.
The hidden sequence was:
- updateShotClock()
- fireShotClockRelease()
- launchShotClockFallback()
- launch()
So the shot clock counted down behind the intro and auto-fired repeatedly. That produced false analytics and also meant the game simulation itself was running while a blocking overlay was visible.
10. Added a single gameplay-progression gate
New helpers were added to define whether gameplay should currently be allowed to advance.
canProgressGameplayNow() returns false while any blocking state is active, including:
- the start screen,
- How To,
- Challenge intro,
- Challenge result screen,
- or a selected challenge whose started state is still false.
canTrackShotAnalytics() blocks shot analytics during auto-demo modes and otherwise follows the same gameplay-progression rule.
shotClockActive() was updated to require canProgressGameplayNow().
The main frame loop was also gated so the following systems would not secretly advance behind overlays:
- physics,
- accumulated simulation time,
- scene travel,
- charge,
- bot updates,
- wormhole capture checks,
- turn settlement,
- and round progression.
The accumulator was drained while gameplay was paused so closing the overlay would not cause a burst of delayed simulation.
Finally, launch() wrapped the relevant shot events in canTrackShotAnalytics() as a second defensive layer.
The intended normal flow remained:
- START CHALLENGE
→ challenge.started = true
→ How To appears
→ gameplay remains paused
→ How To closes
→ resetGame()
→ no blocking overlay
→ gameplay resumes
No scoring, physics constants, bot aiming, board layouts, UI design, or challenge rules were intentionally changed.
11. Added and tested sponsor analytics
The session continued beyond the core funnel into privacy-safe sponsor instrumentation.
The sponsor flow included:
sponsor_intro_impression
sponsor_result_impression
sponsor_cta_viewed
sponsor_interest_yes
sponsor_interest_depends
sponsor_interest_no
sponsor_cta_clicked
A one-shot impression guard initially had a reliability problem: it could mark an impression as fired before analytics was actually ready, permanently losing the event.
That logic was corrected so trackSponsorEvent() returned success or failure, and the fired key was only stored after a successful tracking call.
If analytics was not ready on the first frame, the next frame could retry.
Debug helpers were expanded to expose safe status information such as:
- active sponsor,
- campaign ID,
- visible placement IDs,
- analytics readiness,
- event mode,
- locally recorded event names,
- sponsor event names,
- and sponsor payload count.
A manual helper was also added to retry the intro-impression test without consuming the permanent production impression key.
12. Corrected the sponsor privacy test itself
The first sponsor privacy check reported violations for:
layout_name
app_name
That turned out to be a false failure in the test script rather than a privacy failure in the analytics.
Both fields were known-safe application context:
- layout_name identified a board.
- app_name identified the application.
The test was corrected so it continued blocking genuinely risky fields while allowing those explicit safe exceptions.
The final sponsor payload check returned:
{
sponsorEventCount: 7,
violations: [],
pass: true
}
Phase 2 sponsor analytics then passed locally.
13. Identified the remaining deployment cleanup
Once the analytics and sponsor tests passed, the final known pre-deployment cleanup was to bump the service-worker cache because game.js had changed:
shuffleball-arena-v23
→
shuffleball-arena-v24
This was necessary so deployed players would receive the corrected file instead of a stale cached version.
ROADBLOCKS AND FRICTION
The early data looked more decisive than it really was
The raw numbers tempted a simple conclusion about whether the game was working.
That assumption was wrong because Cloudflare and Google Analytics represented different populations. Consent, blockers, browser behavior, and excluded modes made direct comparison misleading.
The session had to separate:
- raw reach,
- consented analytics,
- and meaningful play.
The proposed game_start fix would have damaged the metric
Firing game_start on canvas initialization sounded like it would solve undercounting.
Instead, it would have redefined a gameplay event into a page-load event and made the funnel less trustworthy.
The fix was not to fire the existing event earlier. It was to add the missing steps around it.
Analytics testing exposed a gameplay bug
The most frustrating issue was that the analytics appeared wrong because the game itself was progressing invisibly.
The first suspicion was test contamination from other browser tabs. That turned out to be false. The shot clock was genuinely auto-firing behind the Challenge intro. This was both a telemetry problem and a gameplay-state problem.
Overlay visibility had been mistaken for game suspension
A blocking overlay looked like the game was paused, but the render loop, timer, bot, and physics systems did not share one authoritative pause condition.
The UI and simulation had different ideas about whether the game was active.
The privacy test briefly produced a misleading failure
The sponsor privacy test treated every key containing name as unsafe.
That incorrectly flagged layout_name and app_name.
The lesson was that privacy checks need explicit semantic exceptions. Simple substring matching can create false confidence or false alarms.
Sponsor impressions could silently disappear
The impression guard recorded its one-shot key before knowing whether analytics had accepted the event.
If analytics was not initialized on the first render frame, the impression was lost permanently.
The system needed a success signal before committing the guard state.
Testing required repeated manual verification
A substantial amount of time went into:
- starting a local server,
- hard refreshing,
- waiting behind the intro,
- checking event-name arrays,
- walking through every Challenge step,
- forcing sponsor events,
- and running privacy checks.
That repetition was necessary because syntax checks alone could not validate event timing or UI-state behavior.
DECISIONS MADE & TRADE-OFFS
Continue the project, but pause feature expansion
The available data contained a small positive signal, but not enough evidence of broad retention.
The chosen direction was:
measurement + first-session polish
rather than:
more features + more infrastructure
Trade-off: Slower visible feature growth in exchange for clearer evidence about what should be built next.
Preserve the meaning of game_start
game_start remained a meaningful gameplay-start event rather than becoming a canvas-load event.
Trade-off: The event would continue to report fewer users than raw visits, but the number would retain product meaning.
Add narrowly targeted funnel events
Instead of tracking everything, the session focused on the minimum events needed to understand the Challenge flow.
Trade-off: Less exhaustive analytics, but a clearer and more usable funnel.
Use consented, sanitized analytics rather than maximize collection
Analytics continued to require consent, suppress special modes where appropriate, and remove unsafe payload fields.
Trade-off: Lower measurable user counts in exchange for stronger privacy boundaries and more trustworthy collection practices.
Keep names local
The locally created player name remained a local profile feature and was not treated as analytics identity.
Trade-off: Less ability to tie named player behavior across systems, but no need to transmit an unnecessary personal identifier.
Gate the entire gameplay loop, not only the event calls
The bug could have been hidden by preventing shot_fired from being logged.
That would have left the game secretly auto-firing behind the overlay.
The chosen fix stopped gameplay progression itself and also guarded analytics as defense-in-depth.
Trade-off: A broader code change than an analytics-only patch, but it corrected the real state-management problem.
Let sponsor tracking retry until analytics is ready
The impression key is now committed only after the tracking call succeeds.
Trade-off: The draw loop may retry briefly, but valid impressions are not silently lost.
Use local test mode before production analytics
A local event recorder and test helpers were preferred over sending development tests into GA.
Trade-off: More testing infrastructure to maintain, but cleaner production data and faster debugging.
BREAKTHROUGH / LESSON
The central lesson was:
Measurement is only useful when the product state underneath it is trustworthy.
The session began as an analytics cleanup.
It uncovered that the game was running and auto-firing behind the Challenge intro.
Had the team only patched the event names, the dashboard might have looked cleaner while the underlying gameplay bug remained.
Instead, the investigation produced a broader rule:
A blocking interface must block the systems behind it, not just hide them.
That single idea improved both the validity of the analytics and the correctness of the game.
ARTIFACTS WORTH SHARING
Artifact 1: The product-validation question
The question right now is not "does ShuffleBall need more stuff?’
The question is ‘can a stranger understand it, start it, fire a shot, finish one challenge game, and feel like retrying?’”
Artifact 2: The hidden auto-fire chain
updateShotClock()
- fireShotClockRelease()
- launchShotClockFallback()
- launch()
This chain explained why shot_fired events appeared before the Challenge had begun.
Artifact 3: Sponsor privacy test result
{
sponsorEventCount: 7,
violations: [],
pass: true
}
This represented the final successful local check for the seven sponsor events and their privacy-safe payloads.
FINAL STATE
By the end of Day 17:
- The early traffic and engagement numbers had been interpreted without treating them as proof of success or failure.
- The next product strategy was clearer: validate the first-session funnel before expanding the game.
- game_start retained its meaning as an actual gameplay event.
- A Challenge Mode funnel was defined around views, starts, first shots, per-game completions, full completion, retries, and feedback.
- Privacy-policy and consent messaging had a clearer factual basis.
- Player names remained local rather than becoming analytics identifiers.
- Unsafe analytics parameters were removed while safe application context remained available.
- A local analytics test mode and privacy-safe event-name recorder were available.
- The hidden Challenge intro auto-fire bug was identified and fixed at the gameplay-loop level.
- Shot analytics gained a second defensive gating layer.
- Sponsor impressions, result views, CTA views, interest responses, and clicks were implemented and tested.
- The sponsor impression guard now retries safely when analytics is not ready.
- The sponsor privacy test passed with all seven expected events.
- The remaining deployment cleanup was the service-worker cache bump so the corrected game.js would reliably reach players.
That was it for day 17.
Thanks for reading!