Initial commit: IronTelemetry.Client SDK
Error monitoring and crash reporting SDK for .NET with: - Automatic exception capture - User journey tracking - Breadcrumb trails - Buffered sending with offline queue - Sample rate control
This commit is contained in:
commit
b5f4907ef7
|
|
@ -0,0 +1,6 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
*.DotSettings.user
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
|
||||||
|
<!-- NuGet Package Properties -->
|
||||||
|
<PackageId>IronTelemetry.Client</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>David H Friedel Jr</Authors>
|
||||||
|
<Company>MarketAlly</Company>
|
||||||
|
<Description>Client SDK for IronTelemetry - Error Monitoring and Crash Reporting. Capture exceptions, track user journeys, and monitor application health with automatic correlation.</Description>
|
||||||
|
<PackageTags>telemetry;error-monitoring;crash-reporting;exceptions;diagnostics;apm;observability</PackageTags>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<RepositoryUrl>https://github.com/ironservices/irontelemetry-client</RepositoryUrl>
|
||||||
|
<PackageProjectUrl>https://www.irontelemetry.com</PackageProjectUrl>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" Condition="Exists('README.md')" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Static API for IronTelemetry.
|
||||||
|
/// Level 0: Drop-in error capture (Init + CaptureException)
|
||||||
|
/// Level 1: Ambient journey correlation (StartJourney + StartStep)
|
||||||
|
/// </summary>
|
||||||
|
public static class IronTelemetry
|
||||||
|
{
|
||||||
|
private static TelemetryClient? _client;
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize IronTelemetry with a DSN.
|
||||||
|
/// </summary>
|
||||||
|
public static void Init(string dsn)
|
||||||
|
{
|
||||||
|
Init(new TelemetryOptions { Dsn = dsn });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize IronTelemetry with options.
|
||||||
|
/// </summary>
|
||||||
|
public static void Init(TelemetryOptions options)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_client = new TelemetryClient(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Level 0 - Exception Capture
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capture an exception and send it to IronTelemetry.
|
||||||
|
/// Automatically correlates with current journey if one exists.
|
||||||
|
/// </summary>
|
||||||
|
public static void CaptureException(Exception ex)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.CaptureException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capture an exception with additional context.
|
||||||
|
/// </summary>
|
||||||
|
public static void CaptureException(Exception ex, Action<ExceptionContext> configure)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
var context = new ExceptionContext();
|
||||||
|
configure(context);
|
||||||
|
_client!.CaptureException(ex, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capture a message.
|
||||||
|
/// </summary>
|
||||||
|
public static void CaptureMessage(string message, TelemetryLevel level = TelemetryLevel.Info)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.CaptureMessage(message, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a breadcrumb to the current context.
|
||||||
|
/// </summary>
|
||||||
|
public static void AddBreadcrumb(string message, string? category = null)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.AddBreadcrumb(new Breadcrumb
|
||||||
|
{
|
||||||
|
Message = message,
|
||||||
|
Category = category ?? "default",
|
||||||
|
Level = BreadcrumbLevel.Info,
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a breadcrumb to the current context.
|
||||||
|
/// </summary>
|
||||||
|
public static void AddBreadcrumb(Breadcrumb breadcrumb)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.AddBreadcrumb(breadcrumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the current user context.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetUser(string? id, string? email = null, string? username = null)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.SetUser(id, email, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a global tag that will be sent with all events.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetTag(string key, string value)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.SetTag(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set extra data that will be sent with all events.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetExtra(string key, object value)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
_client!.SetExtra(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Level 1 - Journey Correlation
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a new journey. All telemetry within this scope will be correlated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The journey name (e.g., "Checkout Flow", "User Onboarding")</param>
|
||||||
|
/// <returns>A disposable scope - dispose to end the journey</returns>
|
||||||
|
/// <example>
|
||||||
|
/// using (IronTelemetry.StartJourney("Checkout Flow"))
|
||||||
|
/// {
|
||||||
|
/// IronTelemetry.SetUser(currentUser.Id);
|
||||||
|
///
|
||||||
|
/// using (IronTelemetry.StartStep("Validate Cart", "business"))
|
||||||
|
/// {
|
||||||
|
/// ValidateCart();
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// using (IronTelemetry.StartStep("Process Payment", "business"))
|
||||||
|
/// {
|
||||||
|
/// ProcessPayment();
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// </example>
|
||||||
|
public static JourneyScope StartJourney(string name)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return JourneyContext.StartJourney(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a step within the current journey.
|
||||||
|
/// If no journey exists, one is created automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The step name (e.g., "Validate Cart", "Process Payment")</param>
|
||||||
|
/// <param name="category">Optional category (e.g., "business", "technical", "navigation")</param>
|
||||||
|
/// <returns>A disposable scope - dispose to end the step</returns>
|
||||||
|
public static StepScope StartStep(string name, string? category = null)
|
||||||
|
{
|
||||||
|
EnsureInitialized();
|
||||||
|
return JourneyContext.StartStep(name, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current journey, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static JourneyScope? CurrentJourney => JourneyContext.Current;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current step, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static StepScope? CurrentStep => JourneyContext.CurrentStep;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current journey ID, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static string? CurrentJourneyId => JourneyContext.CurrentJourneyId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set metadata on the current journey.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetJourneyMetadata(string key, object value)
|
||||||
|
{
|
||||||
|
JourneyContext.SetMetadata(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark the current step as failed.
|
||||||
|
/// </summary>
|
||||||
|
public static void FailCurrentStep(string? reason = null)
|
||||||
|
{
|
||||||
|
JourneyContext.FailCurrentStep(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Flush
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flush any pending events synchronously.
|
||||||
|
/// </summary>
|
||||||
|
public static void Flush(TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
_client?.Flush(timeout ?? TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flush any pending events asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
public static Task FlushAsync(TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
return _client?.FlushAsync(timeout ?? TimeSpan.FromSeconds(5)) ?? Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get whether the SDK is initialized.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsInitialized => _client != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the underlying TelemetryClient instance.
|
||||||
|
/// Returns null if not initialized.
|
||||||
|
/// </summary>
|
||||||
|
public static TelemetryClient? Client => _client;
|
||||||
|
|
||||||
|
private static void EnsureInitialized()
|
||||||
|
{
|
||||||
|
if (_client == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"IronTelemetry has not been initialized. Call IronTelemetry.Init() first.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ambient journey context that automatically flows through async calls.
|
||||||
|
/// Level 1 integration - no manual ID passing required.
|
||||||
|
/// </summary>
|
||||||
|
public static class JourneyContext
|
||||||
|
{
|
||||||
|
private static readonly AsyncLocal<JourneyScope?> _currentJourney = new();
|
||||||
|
private static readonly AsyncLocal<StepScope?> _currentStep = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current journey, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static JourneyScope? Current => _currentJourney.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current step, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static StepScope? CurrentStep => _currentStep.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current journey ID, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static string? CurrentJourneyId => _currentJourney.Value?.JourneyId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current step ID, if any.
|
||||||
|
/// </summary>
|
||||||
|
public static string? CurrentStepId => _currentStep.Value?.StepId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a new journey. All telemetry within this scope will be correlated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The journey name (e.g., "Checkout Flow", "User Onboarding")</param>
|
||||||
|
/// <returns>A disposable scope - dispose to end the journey</returns>
|
||||||
|
public static JourneyScope StartJourney(string name)
|
||||||
|
{
|
||||||
|
var journey = new JourneyScope(name);
|
||||||
|
_currentJourney.Value = journey;
|
||||||
|
return journey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a step within the current journey.
|
||||||
|
/// If no journey exists, one is created automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The step name (e.g., "Validate Cart", "Process Payment")</param>
|
||||||
|
/// <param name="category">Optional category (e.g., "business", "technical", "navigation")</param>
|
||||||
|
/// <returns>A disposable scope - dispose to end the step</returns>
|
||||||
|
public static StepScope StartStep(string name, string? category = null)
|
||||||
|
{
|
||||||
|
// Auto-create journey if none exists
|
||||||
|
if (_currentJourney.Value == null)
|
||||||
|
{
|
||||||
|
StartJourney("Auto Journey");
|
||||||
|
}
|
||||||
|
|
||||||
|
var step = new StepScope(_currentJourney.Value!, name, category);
|
||||||
|
_currentStep.Value = step;
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the user for the current journey.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetUser(string userId, string? email = null, string? username = null)
|
||||||
|
{
|
||||||
|
if (_currentJourney.Value != null)
|
||||||
|
{
|
||||||
|
_currentJourney.Value.SetUser(userId, email, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also set on the global client
|
||||||
|
IronTelemetry.SetUser(userId, email, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add metadata to the current journey.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetMetadata(string key, object value)
|
||||||
|
{
|
||||||
|
_currentJourney.Value?.SetMetadata(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark the current step as failed.
|
||||||
|
/// </summary>
|
||||||
|
public static void FailCurrentStep(string? reason = null)
|
||||||
|
{
|
||||||
|
_currentStep.Value?.Fail(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ClearJourney()
|
||||||
|
{
|
||||||
|
_currentJourney.Value = null;
|
||||||
|
_currentStep.Value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ClearStep()
|
||||||
|
{
|
||||||
|
_currentStep.Value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an active journey scope.
|
||||||
|
/// </summary>
|
||||||
|
public class JourneyScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly DateTime _startTime;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string JourneyId { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string? UserId { get; private set; }
|
||||||
|
public string? UserEmail { get; private set; }
|
||||||
|
public string? Username { get; private set; }
|
||||||
|
public JourneyStatus Status { get; private set; } = JourneyStatus.InProgress;
|
||||||
|
public Dictionary<string, object> Metadata { get; } = new();
|
||||||
|
|
||||||
|
internal JourneyScope(string name)
|
||||||
|
{
|
||||||
|
JourneyId = Guid.NewGuid().ToString();
|
||||||
|
Name = name;
|
||||||
|
_startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Send journey start event
|
||||||
|
TelemetryClient.CurrentClient?.EnqueueJourneyStart(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUser(string userId, string? email = null, string? username = null)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
UserEmail = email;
|
||||||
|
Username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetMetadata(string key, object value)
|
||||||
|
{
|
||||||
|
Metadata[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
Status = JourneyStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail(string? reason = null)
|
||||||
|
{
|
||||||
|
Status = JourneyStatus.Failed;
|
||||||
|
if (reason != null)
|
||||||
|
{
|
||||||
|
Metadata["failureReason"] = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abandon()
|
||||||
|
{
|
||||||
|
Status = JourneyStatus.Abandoned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
var duration = DateTime.UtcNow - _startTime;
|
||||||
|
Metadata["durationMs"] = duration.TotalMilliseconds;
|
||||||
|
|
||||||
|
// If still in progress, mark as completed
|
||||||
|
if (Status == JourneyStatus.InProgress)
|
||||||
|
{
|
||||||
|
Status = JourneyStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send journey end event
|
||||||
|
TelemetryClient.CurrentClient?.EnqueueJourneyEnd(this);
|
||||||
|
|
||||||
|
JourneyContext.ClearJourney();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an active step within a journey.
|
||||||
|
/// </summary>
|
||||||
|
public class StepScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly JourneyScope _journey;
|
||||||
|
private readonly DateTime _startTime;
|
||||||
|
private readonly StepScope? _parentStep;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string StepId { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string? Category { get; }
|
||||||
|
public StepStatus Status { get; private set; } = StepStatus.InProgress;
|
||||||
|
public string? FailureReason { get; private set; }
|
||||||
|
public Dictionary<string, object> Data { get; } = new();
|
||||||
|
|
||||||
|
internal StepScope(JourneyScope journey, string name, string? category)
|
||||||
|
{
|
||||||
|
_journey = journey;
|
||||||
|
_parentStep = JourneyContext.CurrentStep;
|
||||||
|
StepId = Guid.NewGuid().ToString();
|
||||||
|
Name = name;
|
||||||
|
Category = category;
|
||||||
|
_startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Send step start event
|
||||||
|
TelemetryClient.CurrentClient?.EnqueueStepStart(this, journey.JourneyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ParentStepId => _parentStep?.StepId;
|
||||||
|
|
||||||
|
public void SetData(string key, object value)
|
||||||
|
{
|
||||||
|
Data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete()
|
||||||
|
{
|
||||||
|
Status = StepStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fail(string? reason = null)
|
||||||
|
{
|
||||||
|
Status = StepStatus.Failed;
|
||||||
|
FailureReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Skip(string? reason = null)
|
||||||
|
{
|
||||||
|
Status = StepStatus.Skipped;
|
||||||
|
if (reason != null)
|
||||||
|
{
|
||||||
|
Data["skipReason"] = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
var duration = DateTime.UtcNow - _startTime;
|
||||||
|
Data["durationMs"] = duration.TotalMilliseconds;
|
||||||
|
|
||||||
|
// If still in progress, mark as completed
|
||||||
|
if (Status == StepStatus.InProgress)
|
||||||
|
{
|
||||||
|
Status = StepStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send step end event
|
||||||
|
TelemetryClient.CurrentClient?.EnqueueStepEnd(this, _journey.JourneyId);
|
||||||
|
|
||||||
|
// Restore parent step as current
|
||||||
|
JourneyContext.ClearStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Journey status for SDK tracking.
|
||||||
|
/// </summary>
|
||||||
|
public enum JourneyStatus
|
||||||
|
{
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Abandoned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Step status for SDK tracking.
|
||||||
|
/// </summary>
|
||||||
|
public enum StepStatus
|
||||||
|
{
|
||||||
|
InProgress,
|
||||||
|
Completed,
|
||||||
|
Failed,
|
||||||
|
Skipped
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 IronServices
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Telemetry level for messages.
|
||||||
|
/// </summary>
|
||||||
|
public enum TelemetryLevel
|
||||||
|
{
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Breadcrumb level.
|
||||||
|
/// </summary>
|
||||||
|
public enum BreadcrumbLevel
|
||||||
|
{
|
||||||
|
Debug,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A breadcrumb represents an event that happened before an error.
|
||||||
|
/// </summary>
|
||||||
|
public class Breadcrumb
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
public string Category { get; set; } = "default";
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public BreadcrumbLevel Level { get; set; } = BreadcrumbLevel.Info;
|
||||||
|
public Dictionary<string, object>? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional context for exception capture.
|
||||||
|
/// </summary>
|
||||||
|
public class ExceptionContext
|
||||||
|
{
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? UserEmail { get; set; }
|
||||||
|
public string? TraceId { get; set; }
|
||||||
|
public string? SpanId { get; set; }
|
||||||
|
public Dictionary<string, object> Extras { get; set; } = new();
|
||||||
|
|
||||||
|
public ExceptionContext WithUser(string? id, string? email = null)
|
||||||
|
{
|
||||||
|
UserId = id;
|
||||||
|
UserEmail = email;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionContext WithTrace(string traceId, string? spanId = null)
|
||||||
|
{
|
||||||
|
TraceId = traceId;
|
||||||
|
SpanId = spanId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionContext WithExtra(string key, object value)
|
||||||
|
{
|
||||||
|
Extras[key] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File-based offline queue for telemetry items.
|
||||||
|
/// Persists failed sends to disk and retries with exponential backoff.
|
||||||
|
/// </summary>
|
||||||
|
public class OfflineQueue : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _queueFilePath;
|
||||||
|
private readonly int _maxQueueSize;
|
||||||
|
private readonly object _fileLock = new();
|
||||||
|
private readonly SemaphoreSlim _retrySemaphore = new(1, 1);
|
||||||
|
private readonly Timer? _retryTimer;
|
||||||
|
private readonly Func<List<EnvelopeItem>, Task<bool>> _sendFunc;
|
||||||
|
private int _retryAttempt;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = false
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new offline queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sendFunc">Function to send items to server. Returns true on success.</param>
|
||||||
|
/// <param name="queueDirectory">Directory to store queue file. Defaults to app data.</param>
|
||||||
|
/// <param name="maxQueueSize">Maximum items to store. Oldest items dropped when exceeded.</param>
|
||||||
|
/// <param name="enableAutoRetry">Whether to automatically retry sending queued items.</param>
|
||||||
|
public OfflineQueue(
|
||||||
|
Func<List<EnvelopeItem>, Task<bool>> sendFunc,
|
||||||
|
string? queueDirectory = null,
|
||||||
|
int maxQueueSize = 1000,
|
||||||
|
bool enableAutoRetry = true)
|
||||||
|
{
|
||||||
|
_sendFunc = sendFunc;
|
||||||
|
_maxQueueSize = maxQueueSize;
|
||||||
|
|
||||||
|
var directory = queueDirectory ?? GetDefaultQueueDirectory();
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
_queueFilePath = Path.Combine(directory, "telemetry_queue.json");
|
||||||
|
|
||||||
|
if (enableAutoRetry)
|
||||||
|
{
|
||||||
|
// Start retry timer - initial delay 30 seconds, then every 60 seconds
|
||||||
|
_retryTimer = new Timer(
|
||||||
|
_ => _ = RetryQueuedItemsAsync(),
|
||||||
|
null,
|
||||||
|
TimeSpan.FromSeconds(30),
|
||||||
|
TimeSpan.FromSeconds(60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of items currently in the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
var items = LoadQueue();
|
||||||
|
return items.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueue items that failed to send.
|
||||||
|
/// </summary>
|
||||||
|
public void Enqueue(List<EnvelopeItem> items)
|
||||||
|
{
|
||||||
|
if (items.Count == 0) return;
|
||||||
|
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
var queue = LoadQueue();
|
||||||
|
queue.AddRange(items);
|
||||||
|
|
||||||
|
// Trim to max size (remove oldest)
|
||||||
|
while (queue.Count > _maxQueueSize)
|
||||||
|
{
|
||||||
|
queue.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveQueue(queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to send all queued items.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RetryQueuedItemsAsync()
|
||||||
|
{
|
||||||
|
if (!await _retrySemaphore.WaitAsync(0))
|
||||||
|
{
|
||||||
|
// Already retrying
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<EnvelopeItem> items;
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
items = LoadQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
_retryAttempt = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send
|
||||||
|
var success = await _sendFunc(items);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
// Clear the queue
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
SaveQueue(new List<EnvelopeItem>());
|
||||||
|
}
|
||||||
|
_retryAttempt = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Exponential backoff - adjust retry timer
|
||||||
|
_retryAttempt++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_retryAttempt++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_retrySemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all queued items without sending.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
SaveQueue(new List<EnvelopeItem>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all queued items (for display/export).
|
||||||
|
/// </summary>
|
||||||
|
public List<EnvelopeItem> GetQueuedItems()
|
||||||
|
{
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
return LoadQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EnvelopeItem> LoadQueue()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_queueFilePath))
|
||||||
|
{
|
||||||
|
return new List<EnvelopeItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(_queueFilePath);
|
||||||
|
return JsonSerializer.Deserialize<List<EnvelopeItem>>(json, JsonOptions)
|
||||||
|
?? new List<EnvelopeItem>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<EnvelopeItem>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveQueue(List<EnvelopeItem> items)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(items, JsonOptions);
|
||||||
|
File.WriteAllText(_queueFilePath, json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetDefaultQueueDirectory()
|
||||||
|
{
|
||||||
|
return Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"IronTelemetry",
|
||||||
|
"Queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_retryTimer?.Dispose();
|
||||||
|
_retrySemaphore.Dispose();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for offline queue behavior.
|
||||||
|
/// </summary>
|
||||||
|
public class OfflineQueueOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Directory to store queue files. Defaults to LocalApplicationData/IronTelemetry/Queue.
|
||||||
|
/// </summary>
|
||||||
|
public string? QueueDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of items to store in the queue. Oldest items dropped when exceeded.
|
||||||
|
/// Default: 1000
|
||||||
|
/// </summary>
|
||||||
|
public int MaxQueueSize { get; set; } = 1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to automatically retry sending queued items in the background.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAutoRetry { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initial retry delay after a failed send. Default: 30 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan InitialRetryDelay { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum retry delay (for exponential backoff). Default: 5 minutes.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
# IronTelemetry.Client
|
||||||
|
|
||||||
|
Error monitoring and crash reporting SDK for .NET applications.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet add package IronTelemetry.Client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Level 0: Basic Exception Capture
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using IronTelemetry.Client;
|
||||||
|
|
||||||
|
// Initialize with your DSN
|
||||||
|
IronTelemetry.Init("https://pk_live_xxx@irontelemetry.com");
|
||||||
|
|
||||||
|
// Capture exceptions
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DoSomething();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IronTelemetry.CaptureException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or use the extension method
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw ex.Capture();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level 1: Journey Tracking
|
||||||
|
|
||||||
|
Track user journeys to understand the context of errors:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using IronTelemetry.Client;
|
||||||
|
|
||||||
|
// Track a complete user journey
|
||||||
|
using (IronTelemetry.StartJourney("Checkout Flow"))
|
||||||
|
{
|
||||||
|
IronTelemetry.SetUser(currentUser.Id, currentUser.Email);
|
||||||
|
|
||||||
|
using (IronTelemetry.StartStep("Validate Cart", "business"))
|
||||||
|
{
|
||||||
|
ValidateCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (IronTelemetry.StartStep("Process Payment", "business"))
|
||||||
|
{
|
||||||
|
ProcessPayment();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (IronTelemetry.StartStep("Send Confirmation", "notification"))
|
||||||
|
{
|
||||||
|
SendConfirmationEmail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Any exceptions captured during the journey are automatically correlated.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IronTelemetry.Init(new TelemetryOptions
|
||||||
|
{
|
||||||
|
Dsn = "https://pk_live_xxx@irontelemetry.com",
|
||||||
|
Environment = "production",
|
||||||
|
AppVersion = "1.2.3",
|
||||||
|
SampleRate = 1.0, // 100% of events
|
||||||
|
Debug = false,
|
||||||
|
BeforeSend = ex => !ex.Message.Contains("expected error")
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Exception Capture**: Capture and report exceptions with full stack traces
|
||||||
|
- **Journey Tracking**: Track user flows and correlate errors with context
|
||||||
|
- **Breadcrumbs**: Leave a trail of events leading up to an error
|
||||||
|
- **User Context**: Associate errors with specific users
|
||||||
|
- **Tags & Extras**: Add custom metadata to your events
|
||||||
|
- **Buffered Sending**: Events are batched and sent efficiently
|
||||||
|
- **Sample Rate**: Control the volume of events sent
|
||||||
|
- **Before Send Hook**: Filter events before sending
|
||||||
|
|
||||||
|
## Breadcrumbs
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Add breadcrumbs to understand what happened before an error
|
||||||
|
IronTelemetry.AddBreadcrumb("User clicked checkout button", "ui");
|
||||||
|
IronTelemetry.AddBreadcrumb("Payment API called", "http");
|
||||||
|
|
||||||
|
// Or with full control
|
||||||
|
IronTelemetry.AddBreadcrumb(new Breadcrumb
|
||||||
|
{
|
||||||
|
Category = "auth",
|
||||||
|
Message = "User logged in",
|
||||||
|
Level = BreadcrumbLevel.Info,
|
||||||
|
Data = new Dictionary<string, object> { ["userId"] = "123" }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Exception Handling
|
||||||
|
|
||||||
|
For console applications:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
IronTelemetry.Init("your-dsn");
|
||||||
|
TelemetryExtensions.UseUnhandledExceptionHandler();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Methods
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Track a step with automatic error handling
|
||||||
|
TelemetryExtensions.TrackStep("Process Order", () =>
|
||||||
|
{
|
||||||
|
ProcessOrder();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Async version
|
||||||
|
await TelemetryExtensions.TrackStepAsync("Fetch Data", async () =>
|
||||||
|
{
|
||||||
|
await FetchDataAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
// With return value
|
||||||
|
var result = TelemetryExtensions.TrackStep("Calculate Total", () =>
|
||||||
|
{
|
||||||
|
return CalculateTotal();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flushing
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Flush pending events before app shutdown
|
||||||
|
IronTelemetry.Flush();
|
||||||
|
|
||||||
|
// Or async
|
||||||
|
await IronTelemetry.FlushAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](https://www.irontelemetry.com/docs)
|
||||||
|
- [Dashboard](https://www.irontelemetry.com)
|
||||||
|
- [Support](https://www.irontelemetry.com/support)
|
||||||
|
|
@ -0,0 +1,537 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core client for sending telemetry to IronTelemetry API.
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly TelemetryOptions _options;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ConcurrentQueue<Breadcrumb> _breadcrumbs = new();
|
||||||
|
private readonly ConcurrentDictionary<string, string> _tags = new();
|
||||||
|
private readonly ConcurrentDictionary<string, object> _extras = new();
|
||||||
|
private readonly ConcurrentQueue<EnvelopeItem> _pendingItems = new();
|
||||||
|
private readonly ConcurrentQueue<EnvelopeItem> _localLogQueue = new();
|
||||||
|
private readonly SemaphoreSlim _sendSemaphore = new(1, 1);
|
||||||
|
private readonly Timer _flushTimer;
|
||||||
|
private readonly OfflineQueue? _offlineQueue;
|
||||||
|
|
||||||
|
private string? _userId;
|
||||||
|
private string? _userEmail;
|
||||||
|
private string? _userName;
|
||||||
|
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private readonly string _publicKey;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current TelemetryClient instance (used by JourneyContext).
|
||||||
|
/// </summary>
|
||||||
|
internal static TelemetryClient? CurrentClient { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the offline queue for accessing queued items.
|
||||||
|
/// </summary>
|
||||||
|
public OfflineQueue? OfflineQueue => _offlineQueue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all locally captured log items for viewing in AppLogView.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<EnvelopeItem> GetLocalLogItems() => _localLogQueue.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the local log queue.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearLocalLogItems()
|
||||||
|
{
|
||||||
|
while (_localLogQueue.TryDequeue(out _)) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of items to keep in the local log queue.
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxLocalLogItems = 100;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
Converters = { new JsonStringEnumConverter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
public TelemetryClient(TelemetryOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
|
||||||
|
// Parse DSN
|
||||||
|
var (baseUrl, publicKey) = ParseDsn(options.Dsn);
|
||||||
|
_baseUrl = baseUrl;
|
||||||
|
_publicKey = publicKey;
|
||||||
|
|
||||||
|
// Create HTTP client
|
||||||
|
var handler = options.HttpHandler ?? new HttpClientHandler();
|
||||||
|
_httpClient = new HttpClient(handler)
|
||||||
|
{
|
||||||
|
Timeout = options.SendTimeout
|
||||||
|
};
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("X-Telemetry-DSN", options.Dsn);
|
||||||
|
|
||||||
|
// Periodic flush timer (every 5 seconds)
|
||||||
|
_flushTimer = new Timer(_ => _ = FlushAsync(), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
// Initialize offline queue if enabled
|
||||||
|
if (options.EnableOfflineQueue)
|
||||||
|
{
|
||||||
|
_offlineQueue = new OfflineQueue(
|
||||||
|
SendItemsToServerAsync,
|
||||||
|
options.OfflineQueueDirectory,
|
||||||
|
options.MaxOfflineQueueSize,
|
||||||
|
enableAutoRetry: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set as current client
|
||||||
|
CurrentClient = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CaptureException(Exception ex, ExceptionContext? context = null)
|
||||||
|
{
|
||||||
|
if (!ShouldCapture(ex))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-correlate with current journey context
|
||||||
|
var currentJourney = JourneyContext.Current;
|
||||||
|
var currentStep = JourneyContext.CurrentStep;
|
||||||
|
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "exception",
|
||||||
|
ExceptionType = ex.GetType().FullName ?? ex.GetType().Name,
|
||||||
|
Message = ex.Message,
|
||||||
|
StackTrace = ex.StackTrace,
|
||||||
|
AppVersion = _options.AppVersion,
|
||||||
|
AppBuild = _options.AppBuild,
|
||||||
|
Environment = _options.Environment,
|
||||||
|
OsName = GetOsName(),
|
||||||
|
OsVersion = System.Environment.OSVersion.VersionString,
|
||||||
|
RuntimeVersion = RuntimeInformation.FrameworkDescription,
|
||||||
|
UserId = context?.UserId ?? currentJourney?.UserId ?? _userId,
|
||||||
|
UserEmail = context?.UserEmail ?? currentJourney?.UserEmail ?? _userEmail,
|
||||||
|
JourneyId = currentJourney?.JourneyId,
|
||||||
|
StepId = currentStep?.StepId,
|
||||||
|
TraceId = context?.TraceId,
|
||||||
|
SpanId = context?.SpanId,
|
||||||
|
Breadcrumbs = GetBreadcrumbs(),
|
||||||
|
Metadata = MergeMetadata(context?.Extras, currentJourney?.Metadata)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add tags to metadata
|
||||||
|
foreach (var tag in _tags)
|
||||||
|
{
|
||||||
|
item.Metadata[$"tag.{tag.Key}"] = tag.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark current step as failed if there is one
|
||||||
|
currentStep?.Fail(ex.Message);
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
var journeyInfo = currentJourney != null ? $" (journey: {currentJourney.Name})" : "";
|
||||||
|
Console.WriteLine($"[IronTelemetry] Captured exception: {ex.GetType().Name}: {ex.Message}{journeyInfo}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CaptureMessage(string message, TelemetryLevel level)
|
||||||
|
{
|
||||||
|
var currentJourney = JourneyContext.Current;
|
||||||
|
var currentStep = JourneyContext.CurrentStep;
|
||||||
|
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "message",
|
||||||
|
ExceptionType = level.ToString(),
|
||||||
|
Message = message,
|
||||||
|
AppVersion = _options.AppVersion,
|
||||||
|
AppBuild = _options.AppBuild,
|
||||||
|
Environment = _options.Environment,
|
||||||
|
UserId = currentJourney?.UserId ?? _userId,
|
||||||
|
UserEmail = currentJourney?.UserEmail ?? _userEmail,
|
||||||
|
JourneyId = currentJourney?.JourneyId,
|
||||||
|
StepId = currentStep?.StepId,
|
||||||
|
Breadcrumbs = GetBreadcrumbs(),
|
||||||
|
Metadata = new Dictionary<string, object>(_extras)
|
||||||
|
};
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddBreadcrumb(Breadcrumb breadcrumb)
|
||||||
|
{
|
||||||
|
_breadcrumbs.Enqueue(breadcrumb);
|
||||||
|
|
||||||
|
// Trim to max
|
||||||
|
while (_breadcrumbs.Count > _options.MaxBreadcrumbs)
|
||||||
|
{
|
||||||
|
_breadcrumbs.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetUser(string? id, string? email, string? username)
|
||||||
|
{
|
||||||
|
_userId = id;
|
||||||
|
_userEmail = email;
|
||||||
|
_userName = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTag(string key, string value)
|
||||||
|
{
|
||||||
|
_tags[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetExtra(string key, object value)
|
||||||
|
{
|
||||||
|
_extras[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start a step using the legacy API (Level 0 compatibility).
|
||||||
|
/// For Level 1, use JourneyContext.StartStep() instead.
|
||||||
|
/// </summary>
|
||||||
|
public IDisposable StartStep(string name)
|
||||||
|
{
|
||||||
|
return JourneyContext.StartStep(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Journey Context Integration
|
||||||
|
|
||||||
|
internal void EnqueueJourneyStart(JourneyScope journey)
|
||||||
|
{
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "journey_start",
|
||||||
|
JourneyId = journey.JourneyId,
|
||||||
|
Name = journey.Name,
|
||||||
|
UserId = journey.UserId ?? _userId,
|
||||||
|
UserEmail = journey.UserEmail ?? _userEmail,
|
||||||
|
AppVersion = _options.AppVersion,
|
||||||
|
AppBuild = _options.AppBuild,
|
||||||
|
Environment = _options.Environment,
|
||||||
|
OsName = GetOsName(),
|
||||||
|
OsVersion = System.Environment.OSVersion.VersionString,
|
||||||
|
RuntimeVersion = RuntimeInformation.FrameworkDescription
|
||||||
|
};
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Started journey: {journey.Name} ({journey.JourneyId})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EnqueueJourneyEnd(JourneyScope journey)
|
||||||
|
{
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "journey_end",
|
||||||
|
JourneyId = journey.JourneyId,
|
||||||
|
Name = journey.Name,
|
||||||
|
Status = journey.Status.ToString(),
|
||||||
|
UserId = journey.UserId ?? _userId,
|
||||||
|
UserEmail = journey.UserEmail ?? _userEmail,
|
||||||
|
Metadata = new Dictionary<string, object>(journey.Metadata)
|
||||||
|
};
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Ended journey: {journey.Name} ({journey.Status})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EnqueueStepStart(StepScope step, string journeyId)
|
||||||
|
{
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "step_start",
|
||||||
|
JourneyId = journeyId,
|
||||||
|
StepId = step.StepId,
|
||||||
|
ParentStepId = step.ParentStepId,
|
||||||
|
Name = step.Name,
|
||||||
|
Category = step.Category
|
||||||
|
};
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Started step: {step.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EnqueueStepEnd(StepScope step, string journeyId)
|
||||||
|
{
|
||||||
|
var item = new EnvelopeItem
|
||||||
|
{
|
||||||
|
Type = "step_end",
|
||||||
|
JourneyId = journeyId,
|
||||||
|
StepId = step.StepId,
|
||||||
|
Name = step.Name,
|
||||||
|
Status = step.Status.ToString(),
|
||||||
|
Category = step.Category,
|
||||||
|
Data = new Dictionary<string, object>(step.Data)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (step.FailureReason != null)
|
||||||
|
{
|
||||||
|
item.Data["failureReason"] = step.FailureReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingItems.Enqueue(item);
|
||||||
|
AddToLocalLog(item);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Ended step: {step.Name} ({step.Status})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void Flush(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
FlushAsync(timeout).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FlushAsync(TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
if (_pendingItems.IsEmpty)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await _sendSemaphore.WaitAsync(timeout ?? TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = new List<EnvelopeItem>();
|
||||||
|
while (_pendingItems.TryDequeue(out var item))
|
||||||
|
{
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await SendItemsToServerAsync(items);
|
||||||
|
|
||||||
|
if (!success && _offlineQueue != null)
|
||||||
|
{
|
||||||
|
// Queue for retry
|
||||||
|
_offlineQueue.Enqueue(items);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Queued {items.Count} items for offline retry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_sendSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send items to the server. Returns true on success.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> SendItemsToServerAsync(List<EnvelopeItem> items)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var envelope = new { items };
|
||||||
|
var url = $"{_baseUrl}/api/v1/envelope";
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(url, envelope, JsonOptions);
|
||||||
|
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Sent {items.Count} items, status: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (_options.Debug)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[IronTelemetry] Failed to send: {ex.Message}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_flushTimer.Dispose();
|
||||||
|
Flush(TimeSpan.FromSeconds(2));
|
||||||
|
_offlineQueue?.Dispose();
|
||||||
|
_httpClient.Dispose();
|
||||||
|
_sendSemaphore.Dispose();
|
||||||
|
|
||||||
|
if (CurrentClient == this)
|
||||||
|
{
|
||||||
|
CurrentClient = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldCapture(Exception ex)
|
||||||
|
{
|
||||||
|
// Check sample rate
|
||||||
|
if (_options.SampleRate < 1.0 && Random.Shared.NextDouble() > _options.SampleRate)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check before send callback
|
||||||
|
if (_options.BeforeSend != null && !_options.BeforeSend(ex))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BreadcrumbPayload> GetBreadcrumbs()
|
||||||
|
{
|
||||||
|
return _breadcrumbs.Select(b => new BreadcrumbPayload
|
||||||
|
{
|
||||||
|
Timestamp = b.Timestamp,
|
||||||
|
Category = b.Category,
|
||||||
|
Message = b.Message,
|
||||||
|
Level = b.Level.ToString(),
|
||||||
|
Data = b.Data
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, object> MergeMetadata(Dictionary<string, object>? extras, Dictionary<string, object>? journeyMetadata = null)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, object>(_extras);
|
||||||
|
|
||||||
|
if (journeyMetadata != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in journeyMetadata)
|
||||||
|
{
|
||||||
|
result[$"journey.{kvp.Key}"] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extras != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in extras)
|
||||||
|
{
|
||||||
|
result[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetOsName()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "Windows";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "Linux";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "macOS";
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an item to the local log queue for viewing in AppLogView.
|
||||||
|
/// </summary>
|
||||||
|
private void AddToLocalLog(EnvelopeItem item)
|
||||||
|
{
|
||||||
|
_localLogQueue.Enqueue(item);
|
||||||
|
|
||||||
|
// Trim to max size
|
||||||
|
while (_localLogQueue.Count > MaxLocalLogItems)
|
||||||
|
{
|
||||||
|
_localLogQueue.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string baseUrl, string publicKey) ParseDsn(string dsn)
|
||||||
|
{
|
||||||
|
// Format: https://{public_key}@{host}
|
||||||
|
var uri = new Uri(dsn);
|
||||||
|
var publicKey = uri.UserInfo;
|
||||||
|
var host = uri.Host;
|
||||||
|
var baseUrl = $"{uri.Scheme}://{host}";
|
||||||
|
|
||||||
|
return (baseUrl, publicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a telemetry item (exception, message, journey, step).
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeItem
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||||
|
public string? Type { get; set; }
|
||||||
|
public string? JourneyId { get; set; }
|
||||||
|
public string? StepId { get; set; }
|
||||||
|
public string? ParentStepId { get; set; }
|
||||||
|
public string? ExceptionType { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public string? StackTrace { get; set; }
|
||||||
|
public string? AppVersion { get; set; }
|
||||||
|
public string? AppBuild { get; set; }
|
||||||
|
public string? Environment { get; set; }
|
||||||
|
public string? OsName { get; set; }
|
||||||
|
public string? OsVersion { get; set; }
|
||||||
|
public string? DeviceModel { get; set; }
|
||||||
|
public string? RuntimeVersion { get; set; }
|
||||||
|
public string? UserId { get; set; }
|
||||||
|
public string? UserEmail { get; set; }
|
||||||
|
public string? TraceId { get; set; }
|
||||||
|
public string? SpanId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Category { get; set; }
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
public string? DeviceId { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public List<BreadcrumbPayload> Breadcrumbs { get; set; } = [];
|
||||||
|
public Dictionary<string, object> Metadata { get; set; } = [];
|
||||||
|
public Dictionary<string, object> Data { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Breadcrumb payload for telemetry items.
|
||||||
|
/// </summary>
|
||||||
|
public class BreadcrumbPayload
|
||||||
|
{
|
||||||
|
public DateTime? Timestamp { get; set; }
|
||||||
|
public string Category { get; set; } = string.Empty;
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public string Level { get; set; } = "Info";
|
||||||
|
public Dictionary<string, object>? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for IronTelemetry integration.
|
||||||
|
/// </summary>
|
||||||
|
public static class TelemetryExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configure global unhandled exception handling for console applications.
|
||||||
|
/// </summary>
|
||||||
|
public static void UseUnhandledExceptionHandler()
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.ExceptionObject is Exception ex)
|
||||||
|
{
|
||||||
|
IronTelemetry.CaptureException(ex, ctx =>
|
||||||
|
ctx.WithExtra("isTerminating", args.IsTerminating)
|
||||||
|
.WithExtra("source", "UnhandledException"));
|
||||||
|
|
||||||
|
// Flush synchronously since the app may be terminating
|
||||||
|
IronTelemetry.Flush(TimeSpan.FromSeconds(2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskScheduler.UnobservedTaskException += (sender, args) =>
|
||||||
|
{
|
||||||
|
IronTelemetry.CaptureException(args.Exception, ctx =>
|
||||||
|
ctx.WithExtra("source", "UnobservedTaskException"));
|
||||||
|
|
||||||
|
// Mark as observed to prevent app crash
|
||||||
|
args.SetObserved();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capture an exception and return it (for throw expressions).
|
||||||
|
/// </summary>
|
||||||
|
public static Exception Capture(this Exception ex)
|
||||||
|
{
|
||||||
|
IronTelemetry.CaptureException(ex);
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Capture an exception with context and return it.
|
||||||
|
/// </summary>
|
||||||
|
public static Exception Capture(this Exception ex, Action<ExceptionContext> configure)
|
||||||
|
{
|
||||||
|
IronTelemetry.CaptureException(ex, configure);
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a step scope that automatically tracks duration.
|
||||||
|
/// </summary>
|
||||||
|
public static StepScope TimeStep(this TelemetryClient client, string name, string? category = null)
|
||||||
|
{
|
||||||
|
return JourneyContext.StartStep(name, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an action within a tracked step.
|
||||||
|
/// </summary>
|
||||||
|
public static void TrackStep(string name, Action action, string? category = null)
|
||||||
|
{
|
||||||
|
using var step = IronTelemetry.StartStep(name, category);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
step.Fail(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an async action within a tracked step.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task TrackStepAsync(string name, Func<Task> action, string? category = null)
|
||||||
|
{
|
||||||
|
using var step = IronTelemetry.StartStep(name, category);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
step.Fail(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a function within a tracked step and return the result.
|
||||||
|
/// </summary>
|
||||||
|
public static T TrackStep<T>(string name, Func<T> func, string? category = null)
|
||||||
|
{
|
||||||
|
using var step = IronTelemetry.StartStep(name, category);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return func();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
step.Fail(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an async function within a tracked step and return the result.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T> TrackStepAsync<T>(string name, Func<Task<T>> func, string? category = null)
|
||||||
|
{
|
||||||
|
using var step = IronTelemetry.StartStep(name, category);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await func();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
step.Fail(ex.Message);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add Activity trace context to the current telemetry.
|
||||||
|
/// </summary>
|
||||||
|
public static ExceptionContext WithActivity(this ExceptionContext context, Activity? activity = null)
|
||||||
|
{
|
||||||
|
activity ??= Activity.Current;
|
||||||
|
if (activity != null)
|
||||||
|
{
|
||||||
|
context.TraceId = activity.TraceId.ToString();
|
||||||
|
context.SpanId = activity.SpanId.ToString();
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a journey and return the result.
|
||||||
|
/// </summary>
|
||||||
|
public static T RunJourney<T>(string name, Func<T> action)
|
||||||
|
{
|
||||||
|
using var journey = IronTelemetry.StartJourney(name);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = action();
|
||||||
|
journey.Complete();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
journey.Fail(ex.Message);
|
||||||
|
IronTelemetry.CaptureException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute an async journey and return the result.
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<T> RunJourneyAsync<T>(string name, Func<Task<T>> action)
|
||||||
|
{
|
||||||
|
using var journey = IronTelemetry.StartJourney(name);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await action();
|
||||||
|
journey.Complete();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
journey.Fail(ex.Message);
|
||||||
|
IronTelemetry.CaptureException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
namespace IronTelemetry.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for IronTelemetry SDK.
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The DSN (Data Source Name) for your project.
|
||||||
|
/// Format: https://{public_key}@irontelemetry.com
|
||||||
|
/// </summary>
|
||||||
|
public string Dsn { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The environment (e.g., "production", "staging", "development").
|
||||||
|
/// </summary>
|
||||||
|
public string? Environment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The application version.
|
||||||
|
/// </summary>
|
||||||
|
public string? AppVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The application build number.
|
||||||
|
/// </summary>
|
||||||
|
public string? AppBuild { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sample rate for events (0.0 to 1.0). Default is 1.0 (100%).
|
||||||
|
/// </summary>
|
||||||
|
public double SampleRate { get; set; } = 1.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of breadcrumbs to store. Default is 100.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBreadcrumbs { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable debug logging. Default is false.
|
||||||
|
/// </summary>
|
||||||
|
public bool Debug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback to filter exceptions before sending.
|
||||||
|
/// Return false to skip sending the exception.
|
||||||
|
/// </summary>
|
||||||
|
public Func<Exception, bool>? BeforeSend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to attach stack trace to messages. Default is false.
|
||||||
|
/// </summary>
|
||||||
|
public bool AttachStacktrace { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for sending events. Default is 5 seconds.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan SendTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom HTTP handler for testing or proxy scenarios.
|
||||||
|
/// </summary>
|
||||||
|
public HttpMessageHandler? HttpHandler { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable offline queue for failed sends. Default is true.
|
||||||
|
/// When enabled, failed sends are persisted to disk and retried automatically.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableOfflineQueue { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directory to store offline queue. Defaults to LocalApplicationData/IronTelemetry/Queue.
|
||||||
|
/// </summary>
|
||||||
|
public string? OfflineQueueDirectory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum items to store in offline queue. Oldest items dropped when exceeded.
|
||||||
|
/// Default: 1000
|
||||||
|
/// </summary>
|
||||||
|
public int MaxOfflineQueueSize { get; set; } = 1000;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue