284 lines
7.4 KiB
C#
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
|
|
}
|