Runtime debugging should reveal context, ownership, and transitions without flooding logs or hiding the original failure. Instrument the lifecycle before guessing at timing.

Primary ownershipThe smallest system boundary that can explain the failure
Readiness anchorAlways available, but logging targets can be context-sensitive
Version sensitivityVerify private names per client build

Investigate before hooking

  1. Identify the owner of the state, view, resource, callback, or request.
  2. Locate its construction or registration site.
  3. Locate callers and consumers that establish the expected contract.
  4. Locate event firing, property mutation, or result-delivery sites.
  5. Observe the lifecycle across creation, readiness, replacement, and teardown.
  6. Instrument the narrowest boundary with identity and timestamps.
  7. Verify cleanup, duplicate-registration behavior, and stale generations.
  8. Remove temporary instrumentation after the conclusion is supported.

Monkey-patching a broad method can be a controlled probe after this map exists, but it should not be the first way to discover ownership.

High-value context fields

  • Current player type and account identity.
  • Arena identifier and period.
  • Vehicle type, arena vehicle ID, and entity ID when relevant.
  • View alias/class and whether it is still current.
  • Context token, callback owner, and subscription count.
  • Current space, camera/controlled entity, mode, replay, spectator, and realm state.
  • Route/view identity, browser instance generation, and native bridge availability.
  • Virtual resource path, package source, generated accessor, and frontend bundle identity.

Structured diagnostic helper

def _debug(event, **fields):
    if not DEBUG:
        return
    ordered = ['%s=%r' % (key, fields[key]) for key in sorted(fields)]
    _logger.info('[feature] %s %s' % (event, ' '.join(ordered)))

def _trace_transition(name):
    player = BigWorld.player()
    _debug(
        name,
        player_type=player.__class__.__name__ if player else None,
        arena_id=_current_arena_id(),
        token=_arena_token,
    )

Do not hide hook failures

Keep the original call and added behavior in separate try blocks only when preserving the original is essential. Log added-behavior failures with context; do not catch everything around the entire wrapper and return a fabricated value.

Duplicate initialization checks

Log installation markers, subscription counts, loop ownership, and current wrapper identity. Repeated output often reveals duplicate init(), screen reconstruction, or multiple event paths starting the same loop.

Transition test matrix

Test What it exposes
Login → hangar Import and account/cache readiness assumptions
Hangar → battle → hangar Account/avatar state boundaries and cleanup
Disconnect / reconnect Session ownership and stale service references
Rapid view open/close Duplicate subscriptions and retained views
Replay seek Entity recreation and stale callback handling
Optional feature absent Import boundaries and graceful degradation

Common mistakes

  • Logging only “failed” without player, arena, entity, or view context.
  • Printing full payloads, secrets, or high-frequency events.
  • Using broad exception handlers that make a broken feature appear successful.
  • Testing only from a warm hangar with all caches already ready.
  • Changing several lifecycle assumptions before proving which one is wrong.

Verification trail

Reproduce with lifecycle tracing enabled, identify the first incorrect transition, then inspect the producer of that event or state. Remove temporary high-frequency diagnostics before release.

Start with packaging and loader evidence

Before debugging lifecycle or rendering, confirm the mod is physically where the loader expects it, the compiled file is fresh, and a unique startup tag appears exactly once in game.log. A clean syntax compile catches basic failures without a client launch.

For the full build and deployment workflow, see Tooling, Assets & Packaging.

Traceback-bearing exception logs

When a caught exception should reach diagnostic tooling, emit a traceback inside the except block with logger.exception(...), logger.error(..., exc_info=True), or the client’s current-exception logger. A plain logger.error("failed: %s", error) records text but not the traceback.

Expected capability misses are different. If an optional mode or seasonal package is absent in a client where that system is unavailable, the failed import is the feature gate—not an exception report. Keep that path silent or low-noise, and reserve traceback-bearing logging for an unexpected failure after the integration was considered available.

Every shipped mod should expose a module-level version constant such as __version__ so crash reports can identify the exact build even when package metadata is unavailable.

  • Automatic reporters commonly inspect root logging records only when exc_info is present. Text-only error logs may be visible to humans but absent from structured exception reports.
  • Stack-backed client diagnostic helpers may require an explicit stack flag and a reportable severity category before a tool can attribute the issue.
  • If failure occurs wholly inside delegated client code or a worker, the traceback may not include a mod-owned frame. Log with traceback while still inside the mod-owned boundary or use the reporter's explicit API when one is verified.

Runtime owner diagnostics

  • For UI projection failures, log the owning view or controller, route state, resource readiness, model generation, and replay state.
  • For battle-derived values, log the baseline source, validation discriminator, vehicle identity, snapshot timestamp, and final-result reconciliation.
  • For server-changing actions, log the owning context, selected target, submitted action, synchronized confirmation, and teardown reason.

Logger naming

Use explicit logger names so diagnostics remain filterable even when a mod is loaded as a loose file or through a packaged archive. Names should describe the broad runtime owner, not a private repository or one-off feature.

LOG = logging.getLogger('mod.example.gameface')
LOG.debug('route=%s ready=%s width=%s height=%s', route, ready, width, height)

For transition bugs, log the current owner together with the event: account/avatar type, arena ID when available, route/view alias for UI, and whether the client is live or replay-driven.

Exception attribution and version identity

Keep a module-level version constant, preferably __version__, aligned with package metadata. Optional diagnostic collectors need both traceback context and a discoverable module version to attribute failures. The mod must continue to load without a collector.

Log caught unexpected failures inside the active exception handler with a traceback-bearing call such as logger.exception(...), logger.error(..., exc_info=True), or the client's LOG_CURRENT_EXCEPTION(). A text-only error message does not preserve exception frames. Keep the existing fallback and cleanup behavior after recording the failure.

A documented optional import miss is a feature-gating condition rather than an unexpected failure. Keep it quiet or low-noise as appropriate. Distinguish that expected absence from an exception raised by an installed integration during setup or use.

Profile staged game-thread work

Scheduling work through BigWorld.callback defers it but still runs it on the game thread. Staging an expensive calculation is useful only if individual steps are bounded. Measure copy construction, crew initialization, parameter evaluation, serialization, and frontend reconciliation separately before choosing an optimization.

For generator-based calculations, measure advancement as well as generator creation: constructing a generator does not execute its deferred body. Record bounded aggregate counts and inclusive/self timings rather than retaining every sample. Restore only owned instrumentation on stop, timeout, failed setup, and teardown.

Use expensive full-payload correctness comparisons separately from page-opening benchmarks. Otherwise verification work becomes part of the latency being measured. Source checks prove contracts and cleanup; representative in-client measurements are still required to claim performance improvements.

Diagnostic provider provenance

Record provider, units, collection time and live/replay context. The live debug controller samples native statPing(), statLagDetected() and getFPS()[1]. Lag is a separate native flag, not a Python threshold on displayed ping. Recording receives values before panel presentation, so changing downstream HUD text does not rewrite already-recorded samples.

Replay playback uses recorded ping/lag/FPS plus current playback FPS. Positive recorded FPS is formatted as recorded(current); replay ping is not the playback machine's current network latency. Keep recorded values separate from measurements taken during playback.

Latency-derived diagnostics can subtract half a server tick and apply rounding or a minimum floor, unlike the HUD source. A one-millisecond floor is not proof of a measured one-millisecond round trip. Preserve each series' original meaning; do not merge providers under one generic ping label or infer packet loss or command acknowledgment delay from them.

Severity names do not configure the logging bridge

The inspected WotLogHandler selects its backend by exact record.levelno lookup, with entries for standard Python severities. It does not translate an arbitrary numeric level by its display name. If formatting and encoding succeed and an unmapped record such as level 25 reaches emit(), the lookup raises KeyError before the selected backend call. Registering a custom severity name alone does not extend this map.

setupFromXML() uses a supplied nonempty configuration through its dictionary configurator. Its fallback installs this handler and formatter with the requested root threshold, which defaults to INFO. Routing, thresholds, filtering and formatting therefore remain separate conditions. Use standard severity values with the existing named logger; traceback-bearing error records remain a separate requirement for exception attribution. This source contract does not establish the active configuration or successful native log delivery in a particular client.

Selected readable mapping and emission controls with fake records, a formatter, encoding provider and recording backend substitutes checked standard values and rejection of an unmapped value. The original handler base was omitted from the host fixture. No real logging configuration, output, native backend, client startup or Python 2.7 runtime was exercised.