789 lines
22 KiB
C#
789 lines
22 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using IronTelemetry.Client;
|
|
|
|
namespace IronServices.Maui.Controls;
|
|
|
|
#region Enums
|
|
|
|
/// <summary>
|
|
/// Display status for a user journey.
|
|
/// </summary>
|
|
public enum JourneyDisplayStatus
|
|
{
|
|
InProgress,
|
|
Completed,
|
|
Failed,
|
|
Abandoned
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display status for a step within a journey.
|
|
/// </summary>
|
|
public enum StepDisplayStatus
|
|
{
|
|
InProgress,
|
|
Completed,
|
|
Failed,
|
|
Skipped
|
|
}
|
|
|
|
/// <summary>
|
|
/// View mode for the UserJourneyView control.
|
|
/// </summary>
|
|
public enum JourneyViewMode
|
|
{
|
|
/// <summary>
|
|
/// Vertical timeline with nested steps shown with indentation.
|
|
/// </summary>
|
|
Timeline,
|
|
|
|
/// <summary>
|
|
/// Expandable tree hierarchy with collapsible nodes.
|
|
/// </summary>
|
|
Tree,
|
|
|
|
/// <summary>
|
|
/// Horizontal flowchart-style diagram showing step progression.
|
|
/// </summary>
|
|
Flow
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region JourneyItem
|
|
|
|
/// <summary>
|
|
/// Represents a user journey for display in UserJourneyView.
|
|
/// </summary>
|
|
public class JourneyItem : INotifyPropertyChanged
|
|
{
|
|
private bool _isExpanded;
|
|
private bool _isSelected;
|
|
|
|
/// <summary>
|
|
/// Unique journey identifier.
|
|
/// </summary>
|
|
public string JourneyId { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Journey name (e.g., "Checkout Flow", "User Onboarding").
|
|
/// </summary>
|
|
public string Name { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Associated user ID.
|
|
/// </summary>
|
|
public string? UserId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Associated user email.
|
|
/// </summary>
|
|
public string? UserEmail { get; set; }
|
|
|
|
/// <summary>
|
|
/// Current journey status.
|
|
/// </summary>
|
|
public JourneyDisplayStatus Status { get; set; } = JourneyDisplayStatus.InProgress;
|
|
|
|
/// <summary>
|
|
/// When the journey started.
|
|
/// </summary>
|
|
public DateTime StartTime { get; set; }
|
|
|
|
/// <summary>
|
|
/// When the journey ended (null if still in progress).
|
|
/// </summary>
|
|
public DateTime? EndTime { get; set; }
|
|
|
|
/// <summary>
|
|
/// Duration in milliseconds.
|
|
/// </summary>
|
|
public double? DurationMs { get; set; }
|
|
|
|
/// <summary>
|
|
/// Custom metadata attached to the journey.
|
|
/// </summary>
|
|
public Dictionary<string, object> Metadata { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Top-level steps in this journey.
|
|
/// </summary>
|
|
public ObservableCollection<StepItem> Steps { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Breadcrumbs captured during this journey.
|
|
/// </summary>
|
|
public List<BreadcrumbItem> Breadcrumbs { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Exceptions captured during this journey.
|
|
/// </summary>
|
|
public List<ExceptionItem> Exceptions { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// UI state: whether this journey is expanded in tree view.
|
|
/// </summary>
|
|
public bool IsExpanded
|
|
{
|
|
get => _isExpanded;
|
|
set { _isExpanded = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// UI state: whether this journey is currently selected.
|
|
/// </summary>
|
|
public bool IsSelected
|
|
{
|
|
get => _isSelected;
|
|
set { _isSelected = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
#region Computed Properties
|
|
|
|
/// <summary>
|
|
/// Formatted duration for display.
|
|
/// </summary>
|
|
public string DurationDisplay
|
|
{
|
|
get
|
|
{
|
|
if (!DurationMs.HasValue) return "In Progress";
|
|
if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms";
|
|
if (DurationMs.Value < 60000) return $"{DurationMs.Value / 1000:F1}s";
|
|
return $"{DurationMs.Value / 60000:F1}m";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Relative time since journey started.
|
|
/// </summary>
|
|
public string TimeAgo
|
|
{
|
|
get
|
|
{
|
|
var span = DateTime.UtcNow - StartTime;
|
|
if (span.TotalMinutes < 1) return "just now";
|
|
if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago";
|
|
if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago";
|
|
if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago";
|
|
return StartTime.ToLocalTime().ToString("MMM d");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Status text for display.
|
|
/// </summary>
|
|
public string StatusDisplay => Status switch
|
|
{
|
|
JourneyDisplayStatus.InProgress => "In Progress",
|
|
JourneyDisplayStatus.Completed => "Completed",
|
|
JourneyDisplayStatus.Failed => "Failed",
|
|
JourneyDisplayStatus.Abandoned => "Abandoned",
|
|
_ => "Unknown"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Color for status badge.
|
|
/// </summary>
|
|
public Color StatusColor => Status switch
|
|
{
|
|
JourneyDisplayStatus.InProgress => Colors.Blue,
|
|
JourneyDisplayStatus.Completed => Colors.Green,
|
|
JourneyDisplayStatus.Failed => Colors.Red,
|
|
JourneyDisplayStatus.Abandoned => Colors.Gray,
|
|
_ => Colors.Gray
|
|
};
|
|
|
|
/// <summary>
|
|
/// Icon/text for status badge.
|
|
/// </summary>
|
|
public string StatusIcon => Status switch
|
|
{
|
|
JourneyDisplayStatus.InProgress => "...",
|
|
JourneyDisplayStatus.Completed => "OK",
|
|
JourneyDisplayStatus.Failed => "X",
|
|
JourneyDisplayStatus.Abandoned => "-",
|
|
_ => "?"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Total number of steps (including nested).
|
|
/// </summary>
|
|
public int StepCount => CountAllSteps(Steps);
|
|
|
|
/// <summary>
|
|
/// Number of failed steps.
|
|
/// </summary>
|
|
public int FailedStepCount => CountFailedSteps(Steps);
|
|
|
|
/// <summary>
|
|
/// Whether there are any failed steps.
|
|
/// </summary>
|
|
public bool HasFailedSteps => FailedStepCount > 0;
|
|
|
|
/// <summary>
|
|
/// Whether there are any exceptions.
|
|
/// </summary>
|
|
public bool HasExceptions => Exceptions.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Whether a user is associated with this journey.
|
|
/// </summary>
|
|
public bool HasUser => !string.IsNullOrEmpty(UserId);
|
|
|
|
/// <summary>
|
|
/// Start time formatted for display.
|
|
/// </summary>
|
|
public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss");
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private static int CountAllSteps(IEnumerable<StepItem> steps)
|
|
{
|
|
int count = 0;
|
|
foreach (var step in steps)
|
|
{
|
|
count++;
|
|
count += CountAllSteps(step.ChildSteps);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static int CountFailedSteps(IEnumerable<StepItem> steps)
|
|
{
|
|
int count = 0;
|
|
foreach (var step in steps)
|
|
{
|
|
if (step.Status == StepDisplayStatus.Failed) count++;
|
|
count += CountFailedSteps(step.ChildSteps);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a flattened list of all steps for timeline display.
|
|
/// </summary>
|
|
public List<StepItem> GetFlattenedSteps()
|
|
{
|
|
var result = new List<StepItem>();
|
|
FlattenSteps(Steps, result, 0);
|
|
return result;
|
|
}
|
|
|
|
private static void FlattenSteps(IEnumerable<StepItem> steps, List<StepItem> result, int level)
|
|
{
|
|
foreach (var step in steps)
|
|
{
|
|
step.NestingLevel = level;
|
|
result.Add(step);
|
|
FlattenSteps(step.ChildSteps, result, level + 1);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region INotifyPropertyChanged
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region StepItem
|
|
|
|
/// <summary>
|
|
/// Represents a step within a user journey.
|
|
/// </summary>
|
|
public class StepItem : INotifyPropertyChanged
|
|
{
|
|
private bool _isExpanded;
|
|
private bool _isSelected;
|
|
private int _nestingLevel;
|
|
|
|
/// <summary>
|
|
/// Unique step identifier.
|
|
/// </summary>
|
|
public string StepId { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Parent step ID for nested steps.
|
|
/// </summary>
|
|
public string? ParentStepId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Step name (e.g., "Validate Cart", "Process Payment").
|
|
/// </summary>
|
|
public string Name { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Step category (e.g., "business", "technical", "navigation").
|
|
/// </summary>
|
|
public string? Category { get; set; }
|
|
|
|
/// <summary>
|
|
/// Current step status.
|
|
/// </summary>
|
|
public StepDisplayStatus Status { get; set; } = StepDisplayStatus.InProgress;
|
|
|
|
/// <summary>
|
|
/// Reason for failure if status is Failed.
|
|
/// </summary>
|
|
public string? FailureReason { get; set; }
|
|
|
|
/// <summary>
|
|
/// When the step started.
|
|
/// </summary>
|
|
public DateTime StartTime { get; set; }
|
|
|
|
/// <summary>
|
|
/// When the step ended.
|
|
/// </summary>
|
|
public DateTime? EndTime { get; set; }
|
|
|
|
/// <summary>
|
|
/// Duration in milliseconds.
|
|
/// </summary>
|
|
public double? DurationMs { get; set; }
|
|
|
|
/// <summary>
|
|
/// Custom data attached to the step.
|
|
/// </summary>
|
|
public Dictionary<string, object> Data { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// Child steps nested under this step.
|
|
/// </summary>
|
|
public ObservableCollection<StepItem> ChildSteps { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Breadcrumbs captured during this step.
|
|
/// </summary>
|
|
public List<BreadcrumbItem> Breadcrumbs { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// UI state: nesting level for indentation.
|
|
/// </summary>
|
|
public int NestingLevel
|
|
{
|
|
get => _nestingLevel;
|
|
set { _nestingLevel = value; OnPropertyChanged(); OnPropertyChanged(nameof(IndentMargin)); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// UI state: whether this step is expanded in tree view.
|
|
/// </summary>
|
|
public bool IsExpanded
|
|
{
|
|
get => _isExpanded;
|
|
set { _isExpanded = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// UI state: whether this step is currently selected.
|
|
/// </summary>
|
|
public bool IsSelected
|
|
{
|
|
get => _isSelected;
|
|
set { _isSelected = value; OnPropertyChanged(); }
|
|
}
|
|
|
|
#region Computed Properties
|
|
|
|
/// <summary>
|
|
/// Formatted duration for display.
|
|
/// </summary>
|
|
public string DurationDisplay
|
|
{
|
|
get
|
|
{
|
|
if (!DurationMs.HasValue) return "...";
|
|
if (DurationMs.Value < 1000) return $"{DurationMs:F0}ms";
|
|
return $"{DurationMs.Value / 1000:F1}s";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Category for display (defaults to "general").
|
|
/// </summary>
|
|
public string CategoryDisplay => Category ?? "general";
|
|
|
|
/// <summary>
|
|
/// Color for status indicator.
|
|
/// </summary>
|
|
public Color StatusColor => Status switch
|
|
{
|
|
StepDisplayStatus.InProgress => Colors.Blue,
|
|
StepDisplayStatus.Completed => Colors.Green,
|
|
StepDisplayStatus.Failed => Colors.Red,
|
|
StepDisplayStatus.Skipped => Colors.Gray,
|
|
_ => Colors.Gray
|
|
};
|
|
|
|
/// <summary>
|
|
/// Icon/text for status indicator.
|
|
/// </summary>
|
|
public string StatusIcon => Status switch
|
|
{
|
|
StepDisplayStatus.InProgress => "...",
|
|
StepDisplayStatus.Completed => "OK",
|
|
StepDisplayStatus.Failed => "X",
|
|
StepDisplayStatus.Skipped => "-",
|
|
_ => "?"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Margin for indentation based on nesting level.
|
|
/// </summary>
|
|
public Thickness IndentMargin => new(NestingLevel * 20, 0, 0, 0);
|
|
|
|
/// <summary>
|
|
/// Whether this step has child steps.
|
|
/// </summary>
|
|
public bool HasChildren => ChildSteps.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Whether this step has a failure reason.
|
|
/// </summary>
|
|
public bool HasFailureReason => !string.IsNullOrEmpty(FailureReason);
|
|
|
|
/// <summary>
|
|
/// Start time formatted for display.
|
|
/// </summary>
|
|
public string StartTimeDisplay => StartTime.ToLocalTime().ToString("HH:mm:ss.fff");
|
|
|
|
#endregion
|
|
|
|
#region INotifyPropertyChanged
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region BreadcrumbItem
|
|
|
|
/// <summary>
|
|
/// Represents a breadcrumb captured during journey/step execution.
|
|
/// </summary>
|
|
public class BreadcrumbItem
|
|
{
|
|
/// <summary>
|
|
/// When the breadcrumb was captured.
|
|
/// </summary>
|
|
public DateTime Timestamp { get; set; }
|
|
|
|
/// <summary>
|
|
/// Breadcrumb category.
|
|
/// </summary>
|
|
public string Category { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Breadcrumb message.
|
|
/// </summary>
|
|
public string Message { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Log level: "info", "warning", "error", "debug".
|
|
/// </summary>
|
|
public string Level { get; set; } = "Info";
|
|
|
|
/// <summary>
|
|
/// Optional additional data.
|
|
/// </summary>
|
|
public Dictionary<string, object>? Data { get; set; }
|
|
|
|
#region Computed Properties
|
|
|
|
/// <summary>
|
|
/// Timestamp formatted for display.
|
|
/// </summary>
|
|
public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss.fff");
|
|
|
|
/// <summary>
|
|
/// Color based on log level.
|
|
/// </summary>
|
|
public Color LevelColor => Level.ToLower() switch
|
|
{
|
|
"error" => Colors.Red,
|
|
"warning" => Colors.Orange,
|
|
"info" => Colors.Blue,
|
|
"debug" => Colors.Gray,
|
|
_ => Colors.Gray
|
|
};
|
|
|
|
/// <summary>
|
|
/// Level display text.
|
|
/// </summary>
|
|
public string LevelDisplay => Level.ToUpper();
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ExceptionItem
|
|
|
|
/// <summary>
|
|
/// Represents an exception captured during journey execution.
|
|
/// </summary>
|
|
public class ExceptionItem
|
|
{
|
|
/// <summary>
|
|
/// When the exception was captured.
|
|
/// </summary>
|
|
public DateTime Timestamp { get; set; }
|
|
|
|
/// <summary>
|
|
/// Exception type name.
|
|
/// </summary>
|
|
public string ExceptionType { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Exception message.
|
|
/// </summary>
|
|
public string Message { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Stack trace.
|
|
/// </summary>
|
|
public string? StackTrace { get; set; }
|
|
|
|
/// <summary>
|
|
/// Step ID where the exception was captured.
|
|
/// </summary>
|
|
public string? StepId { get; set; }
|
|
|
|
#region Computed Properties
|
|
|
|
/// <summary>
|
|
/// Timestamp formatted for display.
|
|
/// </summary>
|
|
public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss");
|
|
|
|
/// <summary>
|
|
/// Whether there is a stack trace.
|
|
/// </summary>
|
|
public bool HasStackTrace => !string.IsNullOrEmpty(StackTrace);
|
|
|
|
/// <summary>
|
|
/// Short display combining type and message.
|
|
/// </summary>
|
|
public string ShortDisplay => $"{ExceptionType}: {Message}";
|
|
|
|
#endregion
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region JourneyReconstructor
|
|
|
|
/// <summary>
|
|
/// Utility class to reconstruct journey hierarchy from flat EnvelopeItem list.
|
|
/// </summary>
|
|
public static class JourneyReconstructor
|
|
{
|
|
/// <summary>
|
|
/// Reconstruct journey hierarchy from flat EnvelopeItem list.
|
|
/// </summary>
|
|
public static List<JourneyItem> ReconstructJourneys(IEnumerable<EnvelopeItem> items)
|
|
{
|
|
var journeyMap = new Dictionary<string, JourneyItem>();
|
|
var stepMap = new Dictionary<string, StepItem>();
|
|
var journeyBreadcrumbs = new Dictionary<string, List<BreadcrumbItem>>();
|
|
var journeyExceptions = new Dictionary<string, List<ExceptionItem>>();
|
|
var stepToJourney = new Dictionary<string, string>(); // stepId -> journeyId
|
|
|
|
// Process items in timestamp order
|
|
foreach (var item in items.OrderBy(i => i.Timestamp))
|
|
{
|
|
switch (item.Type)
|
|
{
|
|
case "journey_start":
|
|
if (!string.IsNullOrEmpty(item.JourneyId))
|
|
{
|
|
journeyMap[item.JourneyId] = new JourneyItem
|
|
{
|
|
JourneyId = item.JourneyId,
|
|
Name = item.Name ?? "Unknown Journey",
|
|
UserId = item.UserId,
|
|
UserEmail = item.UserEmail,
|
|
StartTime = item.Timestamp,
|
|
Status = JourneyDisplayStatus.InProgress
|
|
};
|
|
journeyBreadcrumbs[item.JourneyId] = new List<BreadcrumbItem>();
|
|
journeyExceptions[item.JourneyId] = new List<ExceptionItem>();
|
|
}
|
|
break;
|
|
|
|
case "journey_end":
|
|
if (!string.IsNullOrEmpty(item.JourneyId) &&
|
|
journeyMap.TryGetValue(item.JourneyId, out var journey))
|
|
{
|
|
journey.EndTime = item.Timestamp;
|
|
journey.Status = ParseJourneyStatus(item.Status);
|
|
if (item.Metadata.TryGetValue("durationMs", out var durationObj))
|
|
{
|
|
journey.DurationMs = Convert.ToDouble(durationObj);
|
|
}
|
|
else
|
|
{
|
|
journey.DurationMs = (item.Timestamp - journey.StartTime).TotalMilliseconds;
|
|
}
|
|
foreach (var kvp in item.Metadata)
|
|
{
|
|
journey.Metadata[kvp.Key] = kvp.Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "step_start":
|
|
if (!string.IsNullOrEmpty(item.StepId))
|
|
{
|
|
var step = new StepItem
|
|
{
|
|
StepId = item.StepId,
|
|
ParentStepId = item.ParentStepId,
|
|
Name = item.Name ?? "Unknown Step",
|
|
Category = item.Category,
|
|
StartTime = item.Timestamp,
|
|
Status = StepDisplayStatus.InProgress
|
|
};
|
|
stepMap[item.StepId] = step;
|
|
if (!string.IsNullOrEmpty(item.JourneyId))
|
|
{
|
|
stepToJourney[item.StepId] = item.JourneyId;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "step_end":
|
|
if (!string.IsNullOrEmpty(item.StepId) &&
|
|
stepMap.TryGetValue(item.StepId, out var endStep))
|
|
{
|
|
endStep.EndTime = item.Timestamp;
|
|
endStep.Status = ParseStepStatus(item.Status);
|
|
if (item.Data.TryGetValue("durationMs", out var durObj))
|
|
{
|
|
endStep.DurationMs = Convert.ToDouble(durObj);
|
|
}
|
|
else
|
|
{
|
|
endStep.DurationMs = (item.Timestamp - endStep.StartTime).TotalMilliseconds;
|
|
}
|
|
if (item.Data.TryGetValue("failureReason", out var reasonObj))
|
|
{
|
|
endStep.FailureReason = reasonObj?.ToString();
|
|
}
|
|
foreach (var kvp in item.Data)
|
|
{
|
|
endStep.Data[kvp.Key] = kvp.Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "exception":
|
|
if (!string.IsNullOrEmpty(item.JourneyId) &&
|
|
journeyExceptions.TryGetValue(item.JourneyId, out var excList))
|
|
{
|
|
excList.Add(new ExceptionItem
|
|
{
|
|
Timestamp = item.Timestamp,
|
|
ExceptionType = item.ExceptionType ?? "Exception",
|
|
Message = item.Message ?? "",
|
|
StackTrace = item.StackTrace,
|
|
StepId = item.StepId
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Collect breadcrumbs
|
|
if (!string.IsNullOrEmpty(item.JourneyId) &&
|
|
item.Breadcrumbs?.Count > 0 &&
|
|
journeyBreadcrumbs.TryGetValue(item.JourneyId, out var bcList))
|
|
{
|
|
foreach (var bc in item.Breadcrumbs)
|
|
{
|
|
bcList.Add(new BreadcrumbItem
|
|
{
|
|
Timestamp = bc.Timestamp ?? item.Timestamp,
|
|
Category = bc.Category,
|
|
Message = bc.Message,
|
|
Level = bc.Level,
|
|
Data = bc.Data
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build step hierarchy
|
|
foreach (var step in stepMap.Values)
|
|
{
|
|
if (!string.IsNullOrEmpty(step.ParentStepId) &&
|
|
stepMap.TryGetValue(step.ParentStepId, out var parentStep))
|
|
{
|
|
parentStep.ChildSteps.Add(step);
|
|
step.NestingLevel = parentStep.NestingLevel + 1;
|
|
}
|
|
}
|
|
|
|
// Associate root-level steps with journeys
|
|
foreach (var step in stepMap.Values)
|
|
{
|
|
if (string.IsNullOrEmpty(step.ParentStepId) &&
|
|
stepToJourney.TryGetValue(step.StepId, out var journeyId) &&
|
|
journeyMap.TryGetValue(journeyId, out var ownerJourney))
|
|
{
|
|
ownerJourney.Steps.Add(step);
|
|
}
|
|
}
|
|
|
|
// Associate breadcrumbs and exceptions
|
|
foreach (var journey in journeyMap.Values)
|
|
{
|
|
if (journeyBreadcrumbs.TryGetValue(journey.JourneyId, out var bcs))
|
|
{
|
|
journey.Breadcrumbs = bcs.OrderBy(b => b.Timestamp).ToList();
|
|
}
|
|
if (journeyExceptions.TryGetValue(journey.JourneyId, out var excs))
|
|
{
|
|
journey.Exceptions = excs.OrderBy(e => e.Timestamp).ToList();
|
|
}
|
|
}
|
|
|
|
return journeyMap.Values.ToList();
|
|
}
|
|
|
|
private static JourneyDisplayStatus ParseJourneyStatus(string? status) => status?.ToLower() switch
|
|
{
|
|
"completed" => JourneyDisplayStatus.Completed,
|
|
"failed" => JourneyDisplayStatus.Failed,
|
|
"abandoned" => JourneyDisplayStatus.Abandoned,
|
|
_ => JourneyDisplayStatus.InProgress
|
|
};
|
|
|
|
private static StepDisplayStatus ParseStepStatus(string? status) => status?.ToLower() switch
|
|
{
|
|
"completed" => StepDisplayStatus.Completed,
|
|
"failed" => StepDisplayStatus.Failed,
|
|
"skipped" => StepDisplayStatus.Skipped,
|
|
_ => StepDisplayStatus.InProgress
|
|
};
|
|
}
|
|
|
|
#endregion
|