Skip to content

SDK Touch Surface

A common pre-integration question is “how invasive is this SDK in my codebase?” The honest answer is one touch-site per SDK concern you decide to use, and concerns are orthogonal — you only touch what you turn on. This page shows what that looks like in practice, using the dogfood game Numbers-Go-Up as a reference.

We’re explicit about this because pre-sales pitches that imply “single-file integration” are misleading. The SDK is a singleton autoload, so you can technically call it from one file — but if you only emit track() from one file, you’re not exercising the SDK, you’re paying for it. Real integration touches a handful of files, and that’s by design: each concern lives next to the domain code that triggers it.

Quest Data is organized as orthogonal concerns. Pick the ones you need; ignore the rest. Each concern has a small Public-API surface and a natural touch-site in your codebase:

ConcernPublic APIWhere it naturally lives
Event Trackingtrack(), track_ui(), track_error(), track_purchase(), set_user_properties()Inside the gameplay code where the event happens — UI buttons, win/lose conditions, screen transitions. Disperse, not centralized.
Balancing / Game Databind_balancing(), bind_balancing_pivot(), get_game_data(), fetch_tables()Wherever you load and parse game-data resources (.tres). Usually 1–2 files for tabular content.
Remote Configfetch_remote_config(), get_config(), config_freshness_seconds(), is_config_stale()A settings_manager.gd or your boot sequence — wherever feature flags live. 1 touch-site.
Achievements & Progressionstart_progression(), complete_progression(), unlock_achievement(), submit_score()A dedicated achievement_tracker.gd / leaderboard_tracker.gd that listens to your existing domain signals. 1–2 files even when the triggers come from a dozen places — see Pro-Pattern below.
Cloud Savessave_game(), load_game(), SAVE_CONFLICT_OVERWRITE etc.Your existing save-system file. 1 touch-site — the SDK is a transport layer, not your save state.
Logginglog_debug() / log_info() / log_warning() / log_error() (+ tagged *_t variants)Anywhere — these replace print() / push_warning() / push_error(). Many touch-sites by design, usually behind a thin tagged-logger wrapper.

Five of these concerns are touch-once-and-forget (Cloud Saves, Remote Config, Achievements, Leaderboards, Balancing). Two are disperse-by-design (Event Tracking, Logging) — they instrument the domain code itself, so the surface area scales with how much you instrument.

Numbers-Go-Up is the dogfood game we build the SDK against. It uses five of six concerns (no Remote Config currently) and has ~11 files with direct QuestData.* calls. Here’s the full inventory:

FileConcernWhat it does
scripts/views/idle_app.gdEvent Tracking, User Properties, LoggingTop-level screen transitions: track_ui(), track("screen_view", ...), set_user_properties()
scripts/systems/scavenging/scavenging_manager.gdEvent Tracking, Cloud Save BridgeDomain-specific track() events on scavenge outcomes
scripts/components/changelog_notification.gdEvent Trackingtrack() when a player opens/dismisses an in-game changelog banner
scripts/data/building_data.gdBalancingOne marker comment for bind_balancing() — the actual call sits in idle_manager
scripts/systems/idle_game/idle_manager.gdBalancing (Pivot), Cloud Save Bridgebind_balancing_pivot() for building_costs, building_production. Bridges saves.
scripts/systems/idle_game/save_manager.gdCloud Savessave_game() / load_game() round-trip with conflict resolution
scripts/systems/analytics/achievement_tracker.gdAchievementsListens to MilestoneManager + ScavengingManager signals, calls unlock_achievement()
scripts/systems/analytics/leaderboard_tracker.gdLeaderboards5 game-side hooks calling submit_score() for W8 leaderboards
scripts/systems/changelog/changelog_manager.gdChangelogs (read-only)Polls QuestData for new patch notes
scripts/components/quest_data_debug_panel.gdDev toolingReads dump_state() for the in-game debug overlay
scripts/utils/idle_logger.gdLogging (wrapper)Thin tagged-logger wrapper. Customer code calls IdleLogger.info(...), which forwards to QuestData.log_info_t("idle", ...)

Files beyond these 11 (~10 more) only call the IdleLogger wrapper — they never see QuestData directly. That’s the Logging concern doing its job: the customer codebase decouples from the SDK behind one thin file.

Anti-Pattern: The Central GameAnalytics.gd Wrapper

Section titled “Anti-Pattern: The Central GameAnalytics.gd Wrapper”

A tempting first instinct is to put all SDK calls behind a project-level wrapper:

# DON'T DO THIS
class_name GameAnalytics
extends RefCounted
static func track_level_complete(level: int, score: int) -> void:
QuestData.track("level_complete", {"level": level, "score": score})
static func track_purchase(item: String, price: int) -> void:
QuestData.track("purchase", {"item": item, "price": price})
# … 80 more wrappers …

Three reasons this is the wrong move:

  1. You re-invent the SDK badly. Quest Data already provides typed entrypoints (track_purchase, track_ui, track_error) for the common cases. Re-wrapping them adds indirection without value.
  2. Your wrapper becomes a god-object. Every new event is a new wrapper function. Reviewers see “GameAnalytics.gd +1 function” on every PR and stop reading. Bugs hide in the wrapper layer.
  3. It hides the call-site context. When track() lives next to the code that triggers the event, the call documents why it’s tracked. When it sits in GameAnalytics.gd, you have to grep to find the trigger.

The exception is Logging — there a thin tagged-logger wrapper is the right pattern (see IdleLogger above). Logging is unique because (a) every project wants a project-specific tag, and (b) the wrapper provides a clean fallback when the SDK isn’t loaded (tests, early autoload init). Don’t generalize from logging to the other concerns.

Pro-Pattern: Concern-Trackers When Triggers Are Disperse

Section titled “Pro-Pattern: Concern-Trackers When Triggers Are Disperse”

When the triggers for a concern come from many domain modules, a single concern-specific tracker file is the right answer — not per-domain-module direct calls, and not a global wrapper.

Numbers-Go-Up’s analytics/achievement_tracker.gd is the canonical example. It listens to signals from MilestoneManager, ScavengingManager, and others, and calls QuestData.unlock_achievement() from one place:

# achievement_tracker.gd (paraphrased)
class_name AchievementTracker
extends Node
func _ready() -> void:
MilestoneManager.milestone_reached.connect(_on_milestone)
ScavengingManager.rare_drop.connect(_on_rare_drop)
func _on_milestone(id: String) -> void:
QuestData.unlock_achievement("milestone_" + id)
func _on_rare_drop(item: String) -> void:
if item == "ancient_amulet":
QuestData.unlock_achievement("first_amulet")

Why this beats both alternatives:

  • Direct calls from MilestoneManager would pollute domain code with analytics knowledge.
  • A central GameAnalytics.gd would mix concerns: tracker for achievements, tracker for leaderboards, tracker for purchases, all in one file.

The rule: one tracker per concern, signal-driven, lives in scripts/systems/analytics/. Domain modules stay clean.

If you’re about to integrate Quest Data, expect:

  • Setup: 3 steps from Godot SDK Setup, zero boilerplate in your code.
  • Per concern you use: 1 natural touch-site (Balancing, Cloud Saves, Remote Config, Achievements, Leaderboards). Total 3–10 LOC per concern.
  • Event Tracking: as many touch-sites as you want to instrument. Don’t try to centralize — call track() next to the gameplay event.
  • Logging: optionally one wrapper file (*_logger.gd) to provide your project’s tag and a no-SDK fallback for tests.

For a dogfooded reference, look at the 11-file inventory above and read the actual files in the linked NgU repo. That’s the real shape — not a marketing pitch.