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);
}
}