irontelemetry-dotnet/JourneyContext.cs

284 lines
7.4 KiB
C#

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
}