753 lines
24 KiB
C#
753 lines
24 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Text;
|
|
using IronTelemetry.Client;
|
|
|
|
namespace IronServices.Maui.Controls;
|
|
|
|
/// <summary>
|
|
/// A page for displaying captured app logs (exceptions, telemetry events) with share and clear functionality.
|
|
/// Use within a NavigationPage for proper toolbar display.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// // Navigate to the log view
|
|
/// await Navigation.PushAsync(new AppLogView
|
|
/// {
|
|
/// TelemetryClient = myTelemetryClient
|
|
/// });
|
|
///
|
|
/// // Or with custom title
|
|
/// await Navigation.PushAsync(new AppLogView
|
|
/// {
|
|
/// TelemetryClient = myTelemetryClient,
|
|
/// Title = "Error Logs"
|
|
/// });
|
|
/// </code>
|
|
/// </example>
|
|
public partial class AppLogView : ContentPage
|
|
{
|
|
private TelemetryClient? _telemetryClient;
|
|
private readonly ObservableCollection<LogItem> _logItems = new();
|
|
private readonly List<LogItem> _manualLogItems = new(); // Items added via AddLog() - preserved across RefreshLogs()
|
|
private LogItem? _selectedItem;
|
|
private bool _enableLiveUpdates;
|
|
|
|
/// <summary>
|
|
/// Creates a new AppLogView instance.
|
|
/// </summary>
|
|
public AppLogView()
|
|
{
|
|
InitializeComponent();
|
|
LogList.BindingContext = _logItems;
|
|
}
|
|
|
|
#region Bindable Properties
|
|
|
|
/// <summary>
|
|
/// The TelemetryClient to pull queued logs from. When set, automatically refreshes the log list.
|
|
/// </summary>
|
|
public static readonly BindableProperty TelemetryClientProperty = BindableProperty.Create(
|
|
nameof(TelemetryClient),
|
|
typeof(TelemetryClient),
|
|
typeof(AppLogView),
|
|
null,
|
|
propertyChanged: OnTelemetryClientChanged);
|
|
|
|
/// <summary>
|
|
/// Gets or sets the TelemetryClient used to retrieve queued log items.
|
|
/// </summary>
|
|
public TelemetryClient? TelemetryClient
|
|
{
|
|
get => (TelemetryClient?)GetValue(TelemetryClientProperty);
|
|
set => SetValue(TelemetryClientProperty, value);
|
|
}
|
|
|
|
private static void OnTelemetryClientChanged(BindableObject bindable, object oldValue, object newValue)
|
|
{
|
|
var view = (AppLogView)bindable;
|
|
view._telemetryClient = newValue as TelemetryClient;
|
|
view.RefreshLogs();
|
|
view.UpdateLiveUpdatesSubscription();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to show the share toolbar item. Default: true.
|
|
/// </summary>
|
|
public static readonly BindableProperty ShowShareButtonProperty = BindableProperty.Create(
|
|
nameof(ShowShareButton),
|
|
typeof(bool),
|
|
typeof(AppLogView),
|
|
true,
|
|
propertyChanged: (b, o, n) => ((AppLogView)b).ShareToolbarItem.IsEnabled = (bool)n);
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the Share toolbar button is visible.
|
|
/// </summary>
|
|
public bool ShowShareButton
|
|
{
|
|
get => (bool)GetValue(ShowShareButtonProperty);
|
|
set => SetValue(ShowShareButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to show the clear toolbar item. Default: true.
|
|
/// </summary>
|
|
public static readonly BindableProperty ShowClearButtonProperty = BindableProperty.Create(
|
|
nameof(ShowClearButton),
|
|
typeof(bool),
|
|
typeof(AppLogView),
|
|
true,
|
|
propertyChanged: (b, o, n) => ((AppLogView)b).ClearToolbarItem.IsEnabled = (bool)n);
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the Clear toolbar button is visible.
|
|
/// </summary>
|
|
public bool ShowClearButton
|
|
{
|
|
get => (bool)GetValue(ShowClearButtonProperty);
|
|
set => SetValue(ShowClearButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to show the Copy for AI toolbar item. Default: true.
|
|
/// </summary>
|
|
public static readonly BindableProperty ShowCopyForAIButtonProperty = BindableProperty.Create(
|
|
nameof(ShowCopyForAIButton),
|
|
typeof(bool),
|
|
typeof(AppLogView),
|
|
true,
|
|
propertyChanged: (b, o, n) => ((AppLogView)b).CopyForAIToolbarItem.IsEnabled = (bool)n);
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the Copy for AI toolbar button is visible.
|
|
/// </summary>
|
|
public bool ShowCopyForAIButton
|
|
{
|
|
get => (bool)GetValue(ShowCopyForAIButtonProperty);
|
|
set => SetValue(ShowCopyForAIButtonProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether to automatically refresh when new items are added to the TelemetryClient. Default: false.
|
|
/// When enabled, polls for new items every 2 seconds.
|
|
/// </summary>
|
|
public static readonly BindableProperty EnableLiveUpdatesProperty = BindableProperty.Create(
|
|
nameof(EnableLiveUpdates),
|
|
typeof(bool),
|
|
typeof(AppLogView),
|
|
false,
|
|
propertyChanged: OnEnableLiveUpdatesChanged);
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether to automatically refresh the log list when new items are added.
|
|
/// </summary>
|
|
public bool EnableLiveUpdates
|
|
{
|
|
get => (bool)GetValue(EnableLiveUpdatesProperty);
|
|
set => SetValue(EnableLiveUpdatesProperty, value);
|
|
}
|
|
|
|
private static void OnEnableLiveUpdatesChanged(BindableObject bindable, object oldValue, object newValue)
|
|
{
|
|
var view = (AppLogView)bindable;
|
|
view._enableLiveUpdates = (bool)newValue;
|
|
view.UpdateLiveUpdatesSubscription();
|
|
}
|
|
|
|
private Timer? _liveUpdateTimer;
|
|
private int _lastKnownItemCount;
|
|
|
|
private void UpdateLiveUpdatesSubscription()
|
|
{
|
|
_liveUpdateTimer?.Dispose();
|
|
_liveUpdateTimer = null;
|
|
|
|
if (_enableLiveUpdates && _telemetryClient != null)
|
|
{
|
|
_lastKnownItemCount = _telemetryClient.GetLocalLogItems().Count;
|
|
_liveUpdateTimer = new Timer(CheckForNewItems, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
|
|
}
|
|
}
|
|
|
|
private void CheckForNewItems(object? state)
|
|
{
|
|
if (_telemetryClient == null) return;
|
|
|
|
var currentCount = _telemetryClient.GetLocalLogItems().Count;
|
|
if (currentCount != _lastKnownItemCount)
|
|
{
|
|
_lastKnownItemCount = currentCount;
|
|
MainThread.BeginInvokeOnMainThread(RefreshLogs);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// Raised when logs are cleared via the Clear button.
|
|
/// </summary>
|
|
public event EventHandler? LogsCleared;
|
|
|
|
/// <summary>
|
|
/// Raised when logs are shared via the Share button.
|
|
/// </summary>
|
|
public event EventHandler? LogsShared;
|
|
|
|
/// <summary>
|
|
/// Raised when a log item is selected from the list.
|
|
/// </summary>
|
|
public event EventHandler<LogItem>? LogSelected;
|
|
|
|
/// <summary>
|
|
/// Raised when the log list is refreshed.
|
|
/// </summary>
|
|
public event EventHandler? LogsRefreshed;
|
|
|
|
/// <summary>
|
|
/// Raised when logs are copied for AI debugging.
|
|
/// </summary>
|
|
public event EventHandler? LogsCopiedForAI;
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
/// <summary>
|
|
/// Refresh the log list from the telemetry client's local log queue.
|
|
/// Preserves manually added items (via AddLog()) and merges with telemetry items.
|
|
/// </summary>
|
|
public void RefreshLogs()
|
|
{
|
|
_logItems.Clear();
|
|
|
|
// Collect all items to display
|
|
var allItems = new List<LogItem>();
|
|
|
|
// Add items from telemetry client
|
|
if (_telemetryClient != null)
|
|
{
|
|
var localItems = _telemetryClient.GetLocalLogItems();
|
|
foreach (var item in localItems)
|
|
{
|
|
allItems.Add(LogItem.FromEnvelopeItem(item));
|
|
}
|
|
}
|
|
|
|
// Add manually added items (preserved across refreshes)
|
|
allItems.AddRange(_manualLogItems);
|
|
|
|
// Sort by timestamp descending and add to observable collection
|
|
foreach (var item in allItems.OrderByDescending(i => i.Timestamp))
|
|
{
|
|
_logItems.Add(item);
|
|
}
|
|
|
|
UpdateCount();
|
|
LogsRefreshed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a log item manually (for local-only logging without TelemetryClient).
|
|
/// These items are preserved across RefreshLogs() calls.
|
|
/// </summary>
|
|
/// <param name="type">Log type: "exception", "message", "info", "warning", etc.</param>
|
|
/// <param name="title">Title or exception type name.</param>
|
|
/// <param name="message">Log message or exception message.</param>
|
|
/// <param name="stackTrace">Optional stack trace for exceptions.</param>
|
|
public void AddLog(string type, string title, string message, string? stackTrace = null)
|
|
{
|
|
var item = new LogItem
|
|
{
|
|
Type = type,
|
|
Title = title,
|
|
Message = message,
|
|
StackTrace = stackTrace,
|
|
Timestamp = DateTime.UtcNow,
|
|
IsManualEntry = true
|
|
};
|
|
|
|
// Store in manual items list (preserved across refreshes)
|
|
_manualLogItems.Add(item);
|
|
|
|
// Also add to display list immediately
|
|
_logItems.Insert(0, item);
|
|
UpdateCount();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an exception as a log entry.
|
|
/// </summary>
|
|
/// <param name="ex">The exception to log.</param>
|
|
public void AddException(Exception ex)
|
|
{
|
|
AddLog("exception", ex.GetType().Name, ex.Message, ex.StackTrace);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear all displayed logs including manually added ones. Does not clear the TelemetryClient's offline queue.
|
|
/// </summary>
|
|
public void ClearLogs()
|
|
{
|
|
_logItems.Clear();
|
|
_manualLogItems.Clear();
|
|
HideDetail();
|
|
UpdateCount();
|
|
LogsCleared?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear all logs including the TelemetryClient's local log queue.
|
|
/// </summary>
|
|
public void ClearAllLogs()
|
|
{
|
|
_telemetryClient?.ClearLocalLogItems();
|
|
_telemetryClient?.OfflineQueue?.Clear();
|
|
ClearLogs();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Export all logs as a formatted string suitable for sharing or saving.
|
|
/// </summary>
|
|
/// <returns>Formatted log export string.</returns>
|
|
public string ExportLogs()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine($"=== App Logs Export ===");
|
|
sb.AppendLine($"Exported: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
|
sb.AppendLine($"Total items: {_logItems.Count}");
|
|
sb.AppendLine();
|
|
|
|
foreach (var item in _logItems)
|
|
{
|
|
sb.AppendLine($"--- [{item.Type.ToUpperInvariant()}] {item.Timestamp:yyyy-MM-dd HH:mm:ss UTC} ---");
|
|
sb.AppendLine($"Title: {item.Title}");
|
|
sb.AppendLine($"Message: {item.Message}");
|
|
|
|
if (!string.IsNullOrEmpty(item.StackTrace))
|
|
{
|
|
sb.AppendLine("Stack Trace:");
|
|
sb.AppendLine(item.StackTrace);
|
|
}
|
|
|
|
sb.AppendLine();
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Export logs in a format optimized for AI debugging assistance.
|
|
/// Includes system context, environment info, and structured log data.
|
|
/// </summary>
|
|
/// <returns>AI-optimized log export string.</returns>
|
|
public string ExportLogsForAI()
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
// Header for AI
|
|
sb.AppendLine("# Application Debug Log for AI Analysis");
|
|
sb.AppendLine();
|
|
sb.AppendLine("Please analyze the following application logs and help identify issues, root causes, and potential fixes.");
|
|
sb.AppendLine();
|
|
|
|
// System Context
|
|
sb.AppendLine("## Environment");
|
|
sb.AppendLine("```");
|
|
sb.AppendLine($"Platform: {DeviceInfo.Platform}");
|
|
sb.AppendLine($"OS Version: {DeviceInfo.VersionString}");
|
|
sb.AppendLine($"Device: {DeviceInfo.Model} ({DeviceInfo.Manufacturer})");
|
|
sb.AppendLine($"Device Type: {DeviceInfo.DeviceType}");
|
|
sb.AppendLine($"App: {AppInfo.Name} v{AppInfo.VersionString} (Build {AppInfo.BuildString})");
|
|
sb.AppendLine($"Framework: .NET MAUI");
|
|
sb.AppendLine($"Captured: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
|
|
sb.AppendLine($"Local Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
|
sb.AppendLine($"Timezone: {TimeZoneInfo.Local.DisplayName}");
|
|
sb.AppendLine("```");
|
|
sb.AppendLine();
|
|
|
|
// Summary
|
|
var exceptions = _logItems.Where(i => i.Type == "exception").ToList();
|
|
var warnings = _logItems.Where(i => i.Type == "warning").ToList();
|
|
var journeyEvents = _logItems.Where(i => i.Type.StartsWith("journey_") || i.Type.StartsWith("step_")).ToList();
|
|
|
|
sb.AppendLine("## Summary");
|
|
sb.AppendLine($"- **Total Entries:** {_logItems.Count}");
|
|
sb.AppendLine($"- **Exceptions:** {exceptions.Count}");
|
|
sb.AppendLine($"- **Warnings:** {warnings.Count}");
|
|
sb.AppendLine($"- **Journey Events:** {journeyEvents.Count}");
|
|
if (_logItems.Count > 0)
|
|
{
|
|
var oldest = _logItems.Min(i => i.Timestamp);
|
|
var newest = _logItems.Max(i => i.Timestamp);
|
|
sb.AppendLine($"- **Time Range:** {oldest:HH:mm:ss} to {newest:HH:mm:ss} UTC ({(newest - oldest).TotalSeconds:F1}s span)");
|
|
}
|
|
sb.AppendLine();
|
|
|
|
// Exceptions first (most important for debugging)
|
|
if (exceptions.Count > 0)
|
|
{
|
|
sb.AppendLine("## Exceptions");
|
|
sb.AppendLine();
|
|
foreach (var item in exceptions.OrderByDescending(i => i.Timestamp))
|
|
{
|
|
sb.AppendLine($"### {item.Title}");
|
|
sb.AppendLine($"**Time:** {item.Timestamp:yyyy-MM-dd HH:mm:ss} UTC");
|
|
if (!string.IsNullOrEmpty(item.JourneyId))
|
|
sb.AppendLine($"**Journey ID:** {item.JourneyId}");
|
|
if (!string.IsNullOrEmpty(item.UserId))
|
|
sb.AppendLine($"**User ID:** {item.UserId}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("**Message:**");
|
|
sb.AppendLine($"```");
|
|
sb.AppendLine(item.Message);
|
|
sb.AppendLine($"```");
|
|
if (!string.IsNullOrEmpty(item.StackTrace))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("**Stack Trace:**");
|
|
sb.AppendLine("```");
|
|
sb.AppendLine(item.StackTrace);
|
|
sb.AppendLine("```");
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
|
|
// All logs in chronological order
|
|
sb.AppendLine("## Full Log (Chronological)");
|
|
sb.AppendLine();
|
|
sb.AppendLine("| Time (UTC) | Type | Title | Message |");
|
|
sb.AppendLine("|------------|------|-------|---------|");
|
|
foreach (var item in _logItems.OrderBy(i => i.Timestamp))
|
|
{
|
|
var msg = item.Message?.Replace("|", "\\|").Replace("\n", " ").Replace("\r", "") ?? "";
|
|
if (msg.Length > 100) msg = msg[..97] + "...";
|
|
var title = item.Title?.Replace("|", "\\|") ?? "";
|
|
if (title.Length > 50) title = title[..47] + "...";
|
|
sb.AppendLine($"| {item.Timestamp:HH:mm:ss.fff} | {item.TypeDisplay} | {title} | {msg} |");
|
|
}
|
|
sb.AppendLine();
|
|
|
|
// Detailed entries for non-exceptions
|
|
var otherItems = _logItems.Where(i => i.Type != "exception").OrderByDescending(i => i.Timestamp).ToList();
|
|
if (otherItems.Count > 0)
|
|
{
|
|
sb.AppendLine("## Other Log Details");
|
|
sb.AppendLine();
|
|
foreach (var item in otherItems)
|
|
{
|
|
sb.AppendLine($"### [{item.TypeDisplay}] {item.Title}");
|
|
sb.AppendLine($"**Time:** {item.Timestamp:yyyy-MM-dd HH:mm:ss.fff} UTC");
|
|
if (!string.IsNullOrEmpty(item.JourneyId))
|
|
sb.AppendLine($"**Journey ID:** {item.JourneyId}");
|
|
if (!string.IsNullOrEmpty(item.Message))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine(item.Message);
|
|
}
|
|
sb.AppendLine();
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current log items as a read-only list.
|
|
/// </summary>
|
|
public IReadOnlyList<LogItem> GetLogs() => _logItems.ToList().AsReadOnly();
|
|
|
|
/// <summary>
|
|
/// Get the count of log items.
|
|
/// </summary>
|
|
public int LogCount => _logItems.Count;
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
private void OnClearClicked(object? sender, EventArgs e)
|
|
{
|
|
ClearLogs();
|
|
}
|
|
|
|
private void OnRefreshClicked(object? sender, EventArgs e)
|
|
{
|
|
RefreshLogs();
|
|
}
|
|
|
|
private async void OnShareClicked(object? sender, EventArgs e)
|
|
{
|
|
if (_logItems.Count == 0)
|
|
{
|
|
await DisplayAlert("No Logs", "There are no logs to share.", "OK");
|
|
return;
|
|
}
|
|
|
|
var content = ExportLogs();
|
|
|
|
await Share.Default.RequestAsync(new ShareTextRequest
|
|
{
|
|
Title = "App Logs",
|
|
Text = content
|
|
});
|
|
|
|
LogsShared?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
private async void OnCopyForAIClicked(object? sender, EventArgs e)
|
|
{
|
|
if (_logItems.Count == 0)
|
|
{
|
|
await DisplayAlert("No Logs", "There are no logs to copy.", "OK");
|
|
return;
|
|
}
|
|
|
|
var content = ExportLogsForAI();
|
|
await Clipboard.Default.SetTextAsync(content);
|
|
|
|
// Show feedback
|
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
|
{
|
|
var originalText = CountLabel.Text;
|
|
CountLabel.Text = "Copied for AI!";
|
|
CountLabel.TextColor = Colors.Green;
|
|
await Task.Delay(1500);
|
|
CountLabel.Text = originalText;
|
|
CountLabel.TextColor = Application.Current?.RequestedTheme == AppTheme.Dark
|
|
? Color.FromArgb("#9CA3AF")
|
|
: Color.FromArgb("#6B7280");
|
|
});
|
|
|
|
LogsCopiedForAI?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
private async void OnLogSelected(object? sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (e.CurrentSelection.FirstOrDefault() is LogItem item)
|
|
{
|
|
_selectedItem = item;
|
|
ShowDetail(item);
|
|
LogSelected?.Invoke(this, item);
|
|
|
|
// Copy log entry to clipboard
|
|
await CopyLogToClipboardAsync(item);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies a log item's details to the clipboard.
|
|
/// </summary>
|
|
private async Task CopyLogToClipboardAsync(LogItem item)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("=== Exception Log ===");
|
|
sb.AppendLine($"Type: {item.Type.ToUpperInvariant()}");
|
|
sb.AppendLine($"Time: {item.Timestamp:yyyy-MM-dd HH:mm:ss UTC}");
|
|
sb.AppendLine($"Title: {item.Title}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("--- Message ---");
|
|
sb.AppendLine(item.Message);
|
|
|
|
if (!string.IsNullOrEmpty(item.JourneyId))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine($"Journey ID: {item.JourneyId}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(item.UserId))
|
|
{
|
|
sb.AppendLine($"User ID: {item.UserId}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(item.StackTrace))
|
|
{
|
|
sb.AppendLine();
|
|
sb.AppendLine("--- Stack Trace ---");
|
|
sb.AppendLine(item.StackTrace);
|
|
}
|
|
|
|
await Clipboard.Default.SetTextAsync(sb.ToString());
|
|
|
|
// Show brief toast/feedback
|
|
await MainThread.InvokeOnMainThreadAsync(async () =>
|
|
{
|
|
var originalText = CountLabel.Text;
|
|
CountLabel.Text = "Copied to clipboard!";
|
|
CountLabel.TextColor = Colors.Green;
|
|
await Task.Delay(1500);
|
|
CountLabel.Text = originalText;
|
|
CountLabel.TextColor = Application.Current?.RequestedTheme == AppTheme.Dark
|
|
? Color.FromArgb("#9CA3AF")
|
|
: Color.FromArgb("#6B7280");
|
|
});
|
|
}
|
|
|
|
private void OnCloseDetailClicked(object? sender, EventArgs e)
|
|
{
|
|
HideDetail();
|
|
LogList.SelectedItem = null;
|
|
}
|
|
|
|
private async void OnCopyStackTraceClicked(object? sender, EventArgs e)
|
|
{
|
|
if (_selectedItem != null)
|
|
{
|
|
await CopyLogToClipboardAsync(_selectedItem);
|
|
|
|
if (sender is Button button)
|
|
{
|
|
var originalText = button.Text;
|
|
button.Text = "Copied!";
|
|
await Task.Delay(1000);
|
|
button.Text = originalText;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Lifecycle
|
|
|
|
protected override void OnAppearing()
|
|
{
|
|
base.OnAppearing();
|
|
RefreshLogs();
|
|
UpdateLiveUpdatesSubscription();
|
|
}
|
|
|
|
protected override void OnDisappearing()
|
|
{
|
|
base.OnDisappearing();
|
|
_liveUpdateTimer?.Dispose();
|
|
_liveUpdateTimer = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private void UpdateCount()
|
|
{
|
|
CountLabel.Text = _logItems.Count == 1 ? "1 item" : $"{_logItems.Count} items";
|
|
}
|
|
|
|
private void ShowDetail(LogItem item)
|
|
{
|
|
DetailTitle.Text = item.Title;
|
|
DetailMessage.Text = item.Message ?? "No message available";
|
|
DetailStackTrace.Text = item.StackTrace ?? "No stack trace available";
|
|
DetailPanel.IsVisible = true;
|
|
}
|
|
|
|
private void HideDetail()
|
|
{
|
|
DetailPanel.IsVisible = false;
|
|
_selectedItem = null;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a log item for display in AppLogView.
|
|
/// </summary>
|
|
public class LogItem
|
|
{
|
|
/// <summary>
|
|
/// UTC timestamp when the log was captured.
|
|
/// </summary>
|
|
public DateTime Timestamp { get; set; }
|
|
|
|
/// <summary>
|
|
/// Log type: "exception", "message", "journey_start", "journey_end", "step_start", "step_end", "info", "warning".
|
|
/// </summary>
|
|
public string Type { get; set; } = "info";
|
|
|
|
/// <summary>
|
|
/// Title or exception type name.
|
|
/// </summary>
|
|
public string Title { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Log message or exception message.
|
|
/// </summary>
|
|
public string Message { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Stack trace for exceptions.
|
|
/// </summary>
|
|
public string? StackTrace { get; set; }
|
|
|
|
/// <summary>
|
|
/// Associated journey ID if part of a user journey.
|
|
/// </summary>
|
|
public string? JourneyId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Associated user ID.
|
|
/// </summary>
|
|
public string? UserId { get; set; }
|
|
|
|
/// <summary>
|
|
/// Whether this item was added manually via AddLog() vs from TelemetryClient.
|
|
/// </summary>
|
|
public bool IsManualEntry { get; set; }
|
|
|
|
/// <summary>
|
|
/// Formatted timestamp for display (local time, HH:mm:ss).
|
|
/// </summary>
|
|
public string TimestampDisplay => Timestamp.ToLocalTime().ToString("HH:mm:ss");
|
|
|
|
/// <summary>
|
|
/// Short type label for display badge.
|
|
/// </summary>
|
|
public string TypeDisplay => Type switch
|
|
{
|
|
"exception" => "ERROR",
|
|
"message" => "MSG",
|
|
"journey_start" => "START",
|
|
"journey_end" => "END",
|
|
"step_start" => "STEP",
|
|
"step_end" => "STEP",
|
|
"warning" => "WARN",
|
|
"info" => "INFO",
|
|
_ => Type.ToUpperInvariant()
|
|
};
|
|
|
|
/// <summary>
|
|
/// Color for type badge.
|
|
/// </summary>
|
|
public Color TypeColor => Type switch
|
|
{
|
|
"exception" => Colors.Red,
|
|
"warning" => Colors.Orange,
|
|
"message" => Colors.Blue,
|
|
"info" => Colors.Teal,
|
|
"journey_start" => Colors.Green,
|
|
"journey_end" => Colors.Green,
|
|
"step_start" => Colors.Purple,
|
|
"step_end" => Colors.Purple,
|
|
_ => Colors.Gray
|
|
};
|
|
|
|
/// <summary>
|
|
/// Create a LogItem from a TelemetryClient EnvelopeItem.
|
|
/// </summary>
|
|
public static LogItem FromEnvelopeItem(EnvelopeItem item)
|
|
{
|
|
return new LogItem
|
|
{
|
|
Timestamp = item.Timestamp,
|
|
Type = item.Type ?? "unknown",
|
|
Title = item.ExceptionType ?? item.Name ?? item.Type ?? "Log Entry",
|
|
Message = item.Message ?? "",
|
|
StackTrace = item.StackTrace,
|
|
JourneyId = item.JourneyId,
|
|
UserId = item.UserId
|
|
};
|
|
}
|
|
}
|