using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; namespace BugReporter { public enum Severity { Low, Normal, High, Crash } /// /// Entry point. Call once (a RuntimeInitializeOnLoadMethod is a good place), then either /// let the floating button drive reports or call yourself. /// /// /// BugReporter.Init(new BugReporterConfig { /// ApiKey = "br_live_xxx", /// Endpoint = "https://us-central1-myproj.cloudfunctions.net/report", /// Enabled = Debug.isDebugBuild, /// }); /// BugReporter.SetMetadata("innings", 2); /// /// public static class BugReporter { private static BugReporterConfig _config; private static LogBuffer _logs; private static ReportSender _sender; private static ClipRecorder _clip; private static readonly Dictionary _metadata = new Dictionary(); private static readonly object _gameLock = new object(); private static string _game = string.Empty; private static readonly object _sessionLock = new object(); private static string _session = string.Empty; public static bool IsActive => _config != null && _config.Enabled; internal static BugReporterConfig Config => _config; internal static string CurrentGame { get { lock (_gameLock) return _game; } } internal static string CurrentSession { get { lock (_sessionLock) return _session; } } public static void Init(BugReporterConfig config) { if (config == null) throw new ArgumentNullException(nameof(config)); if (_config != null) { Debug.LogWarning("[BugReporter] Init called twice — ignoring the second call."); return; } if (!config.Enabled) { // Explicitly disabled (release build). Take no hooks, allocate nothing. _config = config; return; } // Pasted keys/URLs routinely arrive with stray whitespace or a newline — trim before validating, // or the server rejects every report with a confusing 401 (header present but malformed). config.ApiKey = config.ApiKey?.Trim(); config.Endpoint = config.Endpoint?.Trim(); if (string.IsNullOrEmpty(config.ApiKey) || string.IsNullOrEmpty(config.Endpoint)) { Debug.LogError("[BugReporter] ApiKey and Endpoint are required — reporter stays off."); return; } if (!config.ApiKey.StartsWith("br_")) { Debug.LogError($"[BugReporter] ApiKey looks wrong ('{config.ApiKey.Substring(0, Math.Min(8, config.ApiKey.Length))}…') — it must start with br_. Get a fresh key from the dashboard (Settings → Rotate API key). Reporter stays off."); return; } if (string.IsNullOrEmpty(config.BuildVersion)) config.BuildVersion = Application.version; _config = config; lock (_gameLock) { _game = config.GameId?.Trim() ?? string.Empty; } _logs = new LogBuffer(config.LogBufferSize, config.IncludeWarnings); _sender = ReportSender.Create(); if (config.RecordClip) _clip = ClipRecorder.Create(); if (config.ShowReportButton) ReportOverlay.Create(); Debug.Log($"[BugReporter] Ready — build {config.BuildVersion}, {config.LogBufferSize}-line log buffer."); } /// /// Attach live game state to whatever gets reported next. Overwrites the same key. /// Keep values small and JSON-friendly (string / number / bool). /// public static void SetMetadata(string key, object value) { if (!IsActive || string.IsNullOrEmpty(key)) return; lock (_metadata) { _metadata[key] = value; } } public static void ClearMetadata() { lock (_metadata) { _metadata.Clear(); } } /// /// Tag every subsequent report with the game the tester is currently in. Call this when a game scene /// loads (or the hub switches games); it sticks until the next call. One project/API key, many games — /// this is what lets the dashboard split them into separate lists instead of one mixed pile. /// public static void SetGame(string gameId) { lock (_gameLock) { _game = gameId?.Trim() ?? string.Empty; } } /// /// Tag every subsequent report with the multiplayer match/session it belongs to. Call this with the /// SAME id on every device in the match (e.g. the server's transaction/match id) when the match starts, /// and pass null/empty when it ends. Reports sharing a session are linked on the dashboard, so a bug /// filed from both devices shows as one incident with each device's logs/screenshot side by side. /// public static void SetSession(string sessionId) { lock (_sessionLock) { _session = sessionId?.Trim() ?? string.Empty; } } /// /// File a report. Captures a screenshot and the current log buffer, then uploads (with retry, and a /// disk queue if offline). Returns immediately — the work runs on a coroutine. /// public static void Report(string title, string description = null, Severity severity = Severity.Normal) { if (!IsActive || _sender == null) return; if (string.IsNullOrWhiteSpace(title)) title = "(no title)"; Dictionary meta; lock (_metadata) { meta = new Dictionary(_metadata); } // The active scene's asset path (e.g. "Assets/_Games/CRICKET/Scene/Ground.unity") is preserved in // builds — the backend derives the game from the "_Games//" segment, so no per-game wiring is // needed. GetActiveScene is main-thread only; Report runs on the main thread. `game` (from SetGame) // stays an optional override the server prefers when set. var active = SceneManager.GetActiveScene(); string scenePath = string.IsNullOrEmpty(active.path) ? active.name : active.path; _sender.Submit(new ReportPayload { title = title, description = description ?? string.Empty, severity = severity.ToString().ToLowerInvariant(), buildVersion = _config.BuildVersion, game = CurrentGame, session = CurrentSession, scene = scenePath, logs = _logs != null ? _logs.Dump() : string.Empty, clip = _clip != null ? _clip.PackLatest() : null, clipFps = _clip != null ? _clip.Fps : 0, metadata = meta, device = DeviceInfo.Capture(), }); } /// Internal: the overlay asks for this when the user submits. internal static void ReportFromOverlay(string title, string description, Severity severity) => Report(title, description, severity); } }