namespace IronTelemetry.Client; /// /// Ambient journey context that automatically flows through async calls. /// Level 1 integration - no manual ID passing required. /// public static class JourneyContext { private static readonly AsyncLocal _currentJourney = new(); private static readonly AsyncLocal _currentStep = new(); /// /// Gets the current journey, if any. /// public static JourneyScope? Current => _currentJourney.Value; /// /// Gets the current step, if any. /// public static StepScope? CurrentStep => _currentStep.Value; /// /// Gets the current journey ID, if any. /// public static string? CurrentJourneyId => _currentJourney.Value?.JourneyId; /// /// Gets the current step ID, if any. /// public static string? CurrentStepId => _currentStep.Value?.StepId; /// /// Start a new journey. All telemetry within this scope will be correlated. /// /// The journey name (e.g., "Checkout Flow", "User Onboarding") /// A disposable scope - dispose to end the journey public static JourneyScope StartJourney(string name) { var journey = new JourneyScope(name); _currentJourney.Value = journey; return journey; } /// /// Start a step within the current journey. /// If no journey exists, one is created automatically. /// /// The step name (e.g., "Validate Cart", "Process Payment") /// Optional category (e.g., "business", "technical", "navigation") /// A disposable scope - dispose to end the step 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; } /// /// Set the user for the current journey. /// 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); } /// /// Add metadata to the current journey. /// public static void SetMetadata(string key, object value) { _currentJourney.Value?.SetMetadata(key, value); } /// /// Mark the current step as failed. /// 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; } } /// /// Represents an active journey scope. /// 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 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(); } } /// /// Represents an active step within a journey. /// 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 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(); } } /// /// Journey status for SDK tracking. /// public enum JourneyStatus { InProgress, Completed, Failed, Abandoned } /// /// Step status for SDK tracking. /// public enum StepStatus { InProgress, Completed, Failed, Skipped }