tools-unity-sentry-6a673e — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited tools-unity-sentry-6a673e (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Sentry provides error tracking and performance monitoring for Unity games. This skill covers SDK configuration, custom instrumentation, and best practices for mobile games.
// Via Unity Package Manager
// Add to manifest.json:
"io.sentry.unity": "1.5.0"[CreateAssetMenu(fileName = "SentryOptions", menuName = "Sentry/Options")]
public class SentryOptionConfiguration : SentryOptionsConfiguration
{
[SerializeField] private string _dsn;
[SerializeField] private string _environment;
[SerializeField] private float _tracesSampleRate = 0.2f;
[SerializeField] private bool _enableLogDebouncing = true;
public override void Configure(SentryUnityOptions options)
{
options.Dsn = _dsn;
options.Environment = _environment;
options.Release = $"{Application.version}+{BuildNumber}";
// Performance monitoring
options.TracesSampleRate = _tracesSampleRate;
options.AutoStartupTraces = true;
// Log debouncing (prevent spam)
options.EnableLogDebouncing = _enableLogDebouncing;
options.DebounceTimeError = TimeSpan.FromSeconds(1);
// Deduplication
options.DeduplicateMode = DeduplicateMode.All;
// Callbacks
options.SetBeforeSend(ModifyEvent);
options.SetBeforeSendTransaction(ModifyTransaction);
options.SetBeforeBreadcrumb(ModifyBreadcrumb);
// Native crash handling
#if UNITY_ANDROID
options.AndroidNativeInitializationType = NativeInitializationType.BuildTime;
#elif UNITY_IOS
options.IosNativeInitializationType = NativeInitializationType.BuildTime;
#endif
}
private SentryEvent ModifyEvent(SentryEvent sentryEvent)
{
// Add custom context to all events
sentryEvent.SetTag("quality_level", QualitySettings.GetQualityLevel().ToString());
sentryEvent.SetTag("wifi", Application.internetReachability ==
NetworkReachability.ReachableViaLocalAreaNetwork ? "true" : "false");
return sentryEvent;
}
private SentryTransaction ModifyTransaction(SentryTransaction transaction)
{
// Add measurements to all transactions
transaction.SetMeasurement("memory.used", GetUsedMemoryMB(), MeasurementUnit.Information.Megabyte);
return transaction;
}
private Breadcrumb ModifyBreadcrumb(Breadcrumb breadcrumb)
{
// Filter unwanted breadcrumbs
if (breadcrumb.Type == "http")
return null; // Drop HTTP breadcrumbs
return breadcrumb;
}
}public class SentryController
{
public void SetUser(string userId, string email = null)
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new SentryUser
{
Id = userId,
Email = email,
Username = username
};
});
}
public void ClearUser()
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new SentryUser();
});
}
}// Global tags (apply to all events)
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("zone.id", currentZoneId);
scope.SetTag("legend.id", activeLegendId);
scope.SetTag("build.type", Debug.isDebugBuild ? "debug" : "release");
});
// Event-specific tags
SentrySdk.CaptureMessage("Custom event", scope =>
{
scope.SetTag("feature", "combat");
});SentrySdk.ConfigureScope(scope =>
{
scope.SetExtra("player_level", 42);
scope.SetExtra("inventory_count", 150);
scope.SetExtra("party_composition", new[] { "warrior", "mage", "healer" });
});public async UniTask LoadZoneAsync(string zoneId)
{
// Start transaction
var transaction = SentrySdk.StartTransaction(
name: $"Zone:Load:{zoneId}",
operation: "zone.load"
);
// Set as current transaction for automatic span parenting
SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);
try
{
// Add context
transaction.SetTag("zone.id", zoneId);
// Create child spans
using (var span = transaction.StartChild("addressables.load"))
{
await LoadAddressablesAsync();
span.SetMeasurement("asset.count", assetCount);
span.Status = SpanStatus.Ok;
}
using (var span = transaction.StartChild("scene.load"))
{
await LoadSceneAsync();
span.Status = SpanStatus.Ok;
}
transaction.Status = SpanStatus.Ok;
}
catch (Exception ex)
{
transaction.Status = SpanStatus.InternalError;
SentrySdk.CaptureException(ex);
throw;
}
finally
{
transaction.Finish();
}
}public async UniTask ExecuteAbilityAsync(ISpan parentSpan)
{
using var abilitySpan = parentSpan.StartChild("ability.execute");
abilitySpan.SetTag("ability.id", _abilityId);
try
{
using (var targetSpan = abilitySpan.StartChild("targeting.resolve"))
{
var targets = await ResolveTargets();
targetSpan.SetMeasurement("target.count", targets.Count);
}
using (var effectSpan = abilitySpan.StartChild("effects.apply"))
{
foreach (var effect in _effects)
{
using var singleEffect = effectSpan.StartChild($"effect:{effect.Id}");
await ApplyEffect(effect);
}
}
abilitySpan.Status = SpanStatus.Ok;
}
catch
{
abilitySpan.Status = SpanStatus.InternalError;
throw;
}
}var span = transaction.StartChild("custom.operation");
span.SetTag("custom.tag", "value");
// Do work...
span.SetMeasurement("items.processed", itemCount);
span.Status = SpanStatus.Ok;
span.Finish(); // Must call if not using 'using'// Numeric measurements with units
transaction.SetMeasurement("load.time", loadTimeMs, MeasurementUnit.Duration.Millisecond);
transaction.SetMeasurement("memory.used", usedMb, MeasurementUnit.Information.Megabyte);
transaction.SetMeasurement("fps.average", avgFps, MeasurementUnit.None);
transaction.SetMeasurement("asset.count", count, MeasurementUnit.None);
// Available units
MeasurementUnit.None
MeasurementUnit.Duration.Millisecond
MeasurementUnit.Duration.Second
MeasurementUnit.Information.Byte
MeasurementUnit.Information.Kilobyte
MeasurementUnit.Information.Megabyte
MeasurementUnit.Fraction.Percent// Simple message
SentrySdk.AddBreadcrumb("Player entered combat zone");
// With category and level
SentrySdk.AddBreadcrumb(
message: "Zone loaded: Dunmiir",
category: "navigation",
level: BreadcrumbLevel.Info
);
// With custom data
SentrySdk.AddBreadcrumb(
message: "Purchase completed",
category: "monetization",
level: BreadcrumbLevel.Info,
data: new Dictionary<string, string>
{
{ "product_id", productId },
{ "price", price.ToString() },
{ "currency", currency }
}
);
// Different levels
BreadcrumbLevel.Debug
BreadcrumbLevel.Info
BreadcrumbLevel.Warning
BreadcrumbLevel.Error
BreadcrumbLevel.Critical// Recommended categories for games:
"navigation" // Zone changes, scene loads
"combat" // Combat events
"ui" // UI state changes
"monetization" // Purchase events
"quest" // Quest progress
"performance" // Performance events
"network" // Network operations
"state" // State machine transitionstry
{
RiskyOperation();
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
// or with extra context
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("operation", "risky_operation");
scope.SetExtra("input_data", inputData);
});
}// Simple message
SentrySdk.CaptureMessage("Unexpected state reached");
// With level
SentrySdk.CaptureMessage("Memory warning triggered", SentryLevel.Warning);
// With context
SentrySdk.CaptureMessage("Combat desync detected", scope =>
{
scope.SetTag("combat.id", combatId);
scope.SetExtra("player_state", playerState);
scope.Level = SentryLevel.Error;
});public class GameplayException : Exception
{
public string ZoneId { get; }
public string AbilityId { get; }
public GameplayException(string message, string zoneId, string abilityId)
: base(message)
{
ZoneId = zoneId;
AbilityId = abilityId;
}
}
// Capture with context
try
{
ExecuteAbility();
}
catch (GameplayException ex)
{
SentrySdk.CaptureException(ex, scope =>
{
scope.SetTag("zone.id", ex.ZoneId);
scope.SetTag("ability.id", ex.AbilityId);
});
}public class PerformanceMonitor
{
private readonly FrameTiming[] _frameTimings = new FrameTiming[1];
public void CaptureFrame()
{
var transaction = SentrySdk.StartTransaction("Gather Metrics", "performance.metrics");
// Frame timing
if (FrameTimingManager.IsFeatureEnabled())
{
FrameTimingManager.CaptureFrameTimings();
var count = FrameTimingManager.GetLatestTimings(1, _frameTimings);
if (count > 0)
{
transaction.SetMeasurement("frame.cpu", _frameTimings[0].cpuFrameTime,
MeasurementUnit.Duration.Millisecond);
transaction.SetMeasurement("frame.gpu", _frameTimings[0].gpuFrameTime,
MeasurementUnit.Duration.Millisecond);
}
}
// Memory
var usedMemory = Profiler.GetTotalAllocatedMemoryLong() / 1_000_000f;
transaction.SetMeasurement("memory.used", usedMemory, MeasurementUnit.Information.Megabyte);
// FPS
transaction.SetMeasurement("fps", 1f / Time.deltaTime, MeasurementUnit.None);
// Context
transaction.SetTag("zone.id", CurrentZone);
transaction.SetTag("in_combat", InCombat.ToString());
transaction.Finish();
}
}public void DetectHitch(float frameTimeMs)
{
if (frameTimeMs > 100f) // Severe hitch
{
SentrySdk.AddBreadcrumb(
message: $"Severe hitch: {frameTimeMs:F1}ms",
category: "performance",
level: BreadcrumbLevel.Error,
data: new Dictionary<string, string>
{
{ "zone", _currentZone },
{ "in_combat", _inCombat.ToString() },
{ "activity", _currentActivity }
}
);
}
else if (frameTimeMs > 50f) // Major hitch
{
SentrySdk.AddBreadcrumb(
message: $"Major hitch: {frameTimeMs:F1}ms",
category: "performance",
level: BreadcrumbLevel.Warning
);
}
}// In SentryOptionsConfiguration
options.Release = $"{Application.version}+{Changelist}";
options.Environment = "production"; // or "staging", "development"// Sessions are tracked automatically, but you can add context
public void OnSessionStart()
{
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("session.type", isFirstSession ? "new" : "returning");
scope.SetTag("install.age", installAgeDays.ToString());
});
}private void OnEnable()
{
Application.lowMemory += OnLowMemory;
}
private void OnLowMemory()
{
SentrySdk.AddBreadcrumb(
message: "Low memory warning",
category: "performance",
level: BreadcrumbLevel.Warning,
data: new Dictionary<string, string>
{
{ "used_mb", GetUsedMemoryMB().ToString() },
{ "zone", _currentZone }
}
);
}private void OnApplicationPause(bool paused)
{
if (paused)
{
SentrySdk.AddBreadcrumb("App backgrounded", "lifecycle");
SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).Forget();
}
else
{
SentrySdk.AddBreadcrumb("App foregrounded", "lifecycle");
}
}SentrySdk.ConfigureScope(scope =>
{
var reachability = Application.internetReachability;
scope.SetTag("network.type", reachability switch
{
NetworkReachability.NotReachable => "none",
NetworkReachability.ReachableViaLocalAreaNetwork => "wifi",
NetworkReachability.ReachableViaCarrierDataNetwork => "cellular",
_ => "unknown"
});
});# Upload debug symbols after build
sentry-cli debug-files upload \
--org your-org \
--project your-project \
./Library/Bee/artifacts/
# For iOS
sentry-cli debug-files upload \
--org your-org \
--project your-project \
./Builds/iOS/# Upload IL2CPP line number mappings
sentry-cli debug-files upload \
--include-sources \
./Library/Bee/artifacts/il2cpp/zone.load, combat.ability, ui.navigateCaptureInEditor during development| Issue | Solution |
|---|---|
| Events not appearing | Check DSN, network, sample rate |
| Missing stack traces | Upload debug symbols |
| Too many events | Increase debounce time, filter |
| Performance impact | Reduce sample rate, async flush |
| Native crashes unsymbolicated | Upload dSYM/symbols |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.