diff --git a/Controls/AppLogView.xaml b/Controls/AppLogView.xaml index 73691c9..310d2b4 100644 --- a/Controls/AppLogView.xaml +++ b/Controls/AppLogView.xaml @@ -10,6 +10,10 @@ Text="Clear" Order="Secondary" Clicked="OnClearClicked" /> + SetValue(ShowClearButtonProperty, value); } + /// + /// Whether to show the Copy for AI toolbar item. Default: true. + /// + public static readonly BindableProperty ShowCopyForAIButtonProperty = BindableProperty.Create( + nameof(ShowCopyForAIButton), + typeof(bool), + typeof(AppLogView), + true, + propertyChanged: (b, o, n) => ((AppLogView)b).CopyForAIToolbarItem.IsEnabled = (bool)n); + + /// + /// Gets or sets whether the Copy for AI toolbar button is visible. + /// + public bool ShowCopyForAIButton + { + get => (bool)GetValue(ShowCopyForAIButtonProperty); + set => SetValue(ShowCopyForAIButtonProperty, value); + } + /// /// Whether to automatically refresh when new items are added to the TelemetryClient. Default: false. /// When enabled, polls for new items every 2 seconds. @@ -186,6 +205,11 @@ public partial class AppLogView : ContentPage /// public event EventHandler? LogsRefreshed; + /// + /// Raised when logs are copied for AI debugging. + /// + public event EventHandler? LogsCopiedForAI; + #endregion #region Public Methods @@ -313,6 +337,123 @@ public partial class AppLogView : ContentPage return sb.ToString(); } + /// + /// Export logs in a format optimized for AI debugging assistance. + /// Includes system context, environment info, and structured log data. + /// + /// AI-optimized log export string. + 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(); + } + /// /// Get the current log items as a read-only list. /// @@ -356,6 +497,33 @@ public partial class AppLogView : ContentPage 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) diff --git a/IronServices.Maui.csproj b/IronServices.Maui.csproj index da45e07..1e096cd 100644 --- a/IronServices.Maui.csproj +++ b/IronServices.Maui.csproj @@ -16,12 +16,18 @@ IronServices.Maui IronServices.Maui - Control library library for IronServices API (IronLicensing, IronTelemetry and IronNotify) + 1.1.0 + MAUI UI controls for IronServices - includes LoginView, LicenseActivationView, AppLogView for telemetry logs, and UserJourneyView for visualizing user journeys with Timeline, Tree, and Flow view modes. David H Friedel Jr MarketAlly IronServices.Maui false nuget_im.png + README.md + MIT + maui;licensing;telemetry;error-monitoring;user-journeys;ironservices;controls;ui + git + Copyright (c) 2025 MarketAlly 15.0 15.0 diff --git a/README.md b/README.md index b147bd1..27d4eab 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ MAUI controls and services for IronServices (IronLicensing, IronNotify, IronTelemetry). +[![NuGet](https://img.shields.io/nuget/v/IronServices.Maui.svg)](https://www.nuget.org/packages/IronServices.Maui/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + ## Installation ```xml @@ -37,6 +40,15 @@ builder.Services.AddIronServices(options => ## Controls +| Control | Type | Description | +|---------|------|-------------| +| [`LoginView`](#loginview) | ContentView | Drop-in login form | +| [`LicenseActivationView`](#licenseactivationview) | ContentView | License key entry and activation | +| [`AppLogView`](#applogview) | ContentPage | Exception and telemetry log viewer | +| [`UserJourneyView`](#userjourneyview) | ContentPage | User journey visualization with Timeline/Tree/Flow modes | + +--- + ### LoginView A drop-in login form for IronServices authentication. @@ -154,6 +166,7 @@ A full-page log viewer for exceptions and telemetry events. Shows queued items f | `Title` | `string` | `"App Logs"` | Page title (shown in navigation bar). | | `ShowShareButton` | `bool` | `true` | Enable Share toolbar button. | | `ShowClearButton` | `bool` | `true` | Enable Clear toolbar button. | +| `EnableLiveUpdates` | `bool` | `false` | Auto-refresh when new items are added (polls every 2s). | #### Events @@ -182,12 +195,15 @@ A full-page log viewer for exceptions and telemetry events. Shows queued items f | Property | Type | Description | |----------|------|-------------| | `Timestamp` | `DateTime` | UTC timestamp. | -| `Type` | `string` | `"exception"`, `"message"`, `"info"`, `"warning"`, etc. | +| `Type` | `string` | `"exception"`, `"message"`, `"journey_start"`, `"step_start"`, etc. | | `Title` | `string` | Exception type name or message title. | | `Message` | `string` | Log message. | | `StackTrace` | `string?` | Stack trace for exceptions. | | `JourneyId` | `string?` | Associated user journey ID. | | `UserId` | `string?` | Associated user ID. | +| `TypeDisplay` | `string` | Short type label: `"ERROR"`, `"MSG"`, `"START"`, etc. | +| `TypeColor` | `Color` | Color for type badge. | +| `TimestampDisplay` | `string` | Formatted local time (HH:mm:ss). | #### Example @@ -196,7 +212,8 @@ A full-page log viewer for exceptions and telemetry events. Shows queued items f await Navigation.PushAsync(new AppLogView { TelemetryClient = _telemetryClient, - Title = "Error Logs" + Title = "Error Logs", + EnableLiveUpdates = true }); // With event handling @@ -213,9 +230,176 @@ logView.AddLog("warning", "Network Issue", "Connection timeout after 30s"); #### Toolbar +- **Refresh** (in header): Reloads from queue. - **Share** (Primary): Exports logs as text and opens system share sheet. -- **Clear** (Secondary/overflow menu): Clears displayed logs. -- **Refresh** button in summary bar: Reloads from queue. +- **Clear** (Secondary/overflow): Clears displayed logs. + +--- + +### UserJourneyView + +A full-page user journey viewer with multiple visualization modes. Reconstructs journeys from `TelemetryClient` log items and displays steps, breadcrumbs, exceptions, and metadata. + +**Type:** `ContentPage` (standalone page, use with NavigationPage) + +#### View Modes + +| Mode | Description | +|------|-------------| +| `Timeline` | Vertical list with nested steps shown with indentation (default). | +| `Tree` | Expandable tree hierarchy with collapsible journey/step nodes. | +| `Flow` | Horizontal flowchart-style diagram showing step progression. | + +#### Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `TelemetryClient` | `TelemetryClient` | `null` | Client to pull journey data from. Auto-refreshes on set. | +| `Title` | `string` | `"User Journeys"` | Page title. | +| `ViewMode` | `JourneyViewMode` | `Timeline` | Current visualization mode. | +| `EnableLiveUpdates` | `bool` | `false` | Auto-refresh when new items are added (polls every 2s). | +| `ShowShareButton` | `bool` | `true` | Enable Share toolbar button. | +| `ShowClearButton` | `bool` | `true` | Enable Clear toolbar button. | +| `UserIdFilter` | `string?` | `null` | Filter to show only journeys for a specific user. | + +#### Events + +| Event | Args | Description | +|-------|------|-------------| +| `JourneySelected` | `JourneyItem` | User selected a journey from the list. | +| `StepSelected` | `StepItem` | User selected a step in the detail panel. | +| `JourneysRefreshed` | `EventArgs` | Journey list was refreshed. | +| `JourneysCleared` | `EventArgs` | Journeys were cleared. | +| `JourneysShared` | `EventArgs` | Journeys were exported/shared. | + +#### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `RefreshJourneys()` | `void` | Reload journeys from TelemetryClient. | +| `ClearJourneys()` | `void` | Clear displayed journeys. | +| `ClearAllJourneys()` | `void` | Clear displayed journeys AND the TelemetryClient queue. | +| `ExportJourneys()` | `string` | Export all journeys as formatted text. | +| `GetJourneys()` | `IReadOnlyList` | Get current journey items. | +| `JourneyCount` | `int` | Number of journeys. | + +#### JourneyItem Properties + +| Property | Type | Description | +|----------|------|-------------| +| `JourneyId` | `string` | Unique journey identifier. | +| `Name` | `string` | Journey name (e.g., "Checkout Flow"). | +| `UserId` | `string?` | Associated user ID. | +| `UserEmail` | `string?` | Associated user email. | +| `Status` | `JourneyDisplayStatus` | `InProgress`, `Completed`, `Failed`, `Abandoned`. | +| `StartTime` | `DateTime` | When the journey started. | +| `EndTime` | `DateTime?` | When the journey ended (null if in progress). | +| `DurationMs` | `double?` | Duration in milliseconds. | +| `DurationDisplay` | `string` | Formatted duration: `"125ms"`, `"2.3s"`, `"1.5m"`. | +| `TimeAgo` | `string` | Relative time: `"just now"`, `"5m ago"`, `"2h ago"`. | +| `StatusDisplay` | `string` | Human-readable status text. | +| `StatusColor` | `Color` | Color for status badge (Blue/Green/Red/Gray). | +| `StatusIcon` | `string` | Icon text: `"..."`, `"OK"`, `"X"`, `"-"`. | +| `Steps` | `ObservableCollection` | Top-level steps in this journey. | +| `Breadcrumbs` | `List` | Breadcrumbs captured during journey. | +| `Exceptions` | `List` | Exceptions captured during journey. | +| `Metadata` | `Dictionary` | Custom metadata. | +| `StepCount` | `int` | Total steps (including nested). | +| `FailedStepCount` | `int` | Number of failed steps. | +| `HasFailedSteps` | `bool` | Whether any steps failed. | +| `HasExceptions` | `bool` | Whether any exceptions occurred. | +| `HasUser` | `bool` | Whether a user is associated. | + +#### StepItem Properties + +| Property | Type | Description | +|----------|------|-------------| +| `StepId` | `string` | Unique step identifier. | +| `ParentStepId` | `string?` | Parent step ID for nested steps. | +| `Name` | `string` | Step name (e.g., "Process Payment"). | +| `Category` | `string?` | Category: `"business"`, `"technical"`, `"navigation"`. | +| `Status` | `StepDisplayStatus` | `InProgress`, `Completed`, `Failed`, `Skipped`. | +| `FailureReason` | `string?` | Reason for failure. | +| `StartTime` | `DateTime` | When the step started. | +| `EndTime` | `DateTime?` | When the step ended. | +| `DurationMs` | `double?` | Duration in milliseconds. | +| `DurationDisplay` | `string` | Formatted duration. | +| `StatusColor` | `Color` | Color for status indicator. | +| `StatusIcon` | `string` | Icon text. | +| `ChildSteps` | `ObservableCollection` | Nested child steps. | +| `NestingLevel` | `int` | Depth level for indentation. | +| `IndentMargin` | `Thickness` | Calculated margin for indentation. | + +#### BreadcrumbItem Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Timestamp` | `DateTime` | When captured. | +| `Category` | `string` | Breadcrumb category. | +| `Message` | `string` | Breadcrumb message. | +| `Level` | `string` | Log level: `"info"`, `"warning"`, `"error"`, `"debug"`. | +| `LevelColor` | `Color` | Color based on level. | +| `Data` | `Dictionary?` | Additional data. | + +#### ExceptionItem Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Timestamp` | `DateTime` | When captured. | +| `ExceptionType` | `string` | Exception type name. | +| `Message` | `string` | Exception message. | +| `StackTrace` | `string?` | Stack trace. | +| `StepId` | `string?` | Step ID where exception occurred. | +| `HasStackTrace` | `bool` | Whether stack trace is available. | + +#### Example + +```csharp +// Basic usage +await Navigation.PushAsync(new UserJourneyView +{ + TelemetryClient = _telemetryClient, + EnableLiveUpdates = true +}); + +// With view mode and filtering +var journeyView = new UserJourneyView +{ + TelemetryClient = _telemetryClient, + ViewMode = JourneyViewMode.Tree, + UserIdFilter = currentUser.Id +}; +await Navigation.PushAsync(journeyView); + +// Event handling +var journeyView = new UserJourneyView { TelemetryClient = _telemetryClient }; +journeyView.JourneySelected += (s, journey) => +{ + Debug.WriteLine($"Selected: {journey.Name} ({journey.StepCount} steps)"); +}; +journeyView.StepSelected += (s, step) => +{ + Debug.WriteLine($"Step: {step.Name} - {step.StatusDisplay}"); +}; +await Navigation.PushAsync(journeyView); +``` + +#### Detail Panel + +When a journey is selected, a detail panel shows: +- **Status, Duration, User** - Summary info +- **Steps** - List of steps with status indicators and timing +- **Breadcrumbs** - Timestamped breadcrumb trail +- **Exceptions** - Exception details (tap to copy) +- **Metadata** - Key-value pairs +- **Copy to Clipboard** - Export journey details + +#### Toolbar + +- **Timeline/Tree/Flow** buttons - Switch view modes +- **Share** (Primary) - Export journeys as text +- **Clear** (Secondary) - Clear displayed journeys +- **Refresh** - Reload from TelemetryClient --- @@ -310,7 +494,20 @@ private async void OnViewLogsClicked(object sender, EventArgs e) var telemetry = Handler.MauiContext.Services.GetService(); await Navigation.PushAsync(new AppLogView { - TelemetryClient = telemetry + TelemetryClient = telemetry, + EnableLiveUpdates = true + }); +} + +// SettingsPage.xaml.cs - Navigate to journeys +private async void OnViewJourneysClicked(object sender, EventArgs e) +{ + var telemetry = Handler.MauiContext.Services.GetService(); + await Navigation.PushAsync(new UserJourneyView + { + TelemetryClient = telemetry, + EnableLiveUpdates = true, + ViewMode = JourneyViewMode.Timeline }); } ``` @@ -329,6 +526,8 @@ All controls use MAUI resource dictionaries. Override these colors in your `App. #919191 #6E6E6E #404040 +#374151 +#1F2937 #212121 #FFFFFF ``` @@ -342,8 +541,8 @@ Controls support light/dark themes via `AppThemeBinding`. | Platform | Minimum Version | |----------|-----------------| | Android | API 21 (5.0) | -| iOS | 14.0 | -| macOS | 11.0 | +| iOS | 15.0 | +| macOS | 15.0 | | Windows | 10.0.17763.0 | --- @@ -352,11 +551,11 @@ Controls support light/dark themes via `AppThemeBinding`. - `IronServices.Client` - Core API client - `IronLicensing.Client` - License validation -- `IronTelemetry.Client` - Telemetry and logging +- `IronTelemetry.Client` - Telemetry, journeys, and logging - `Microsoft.Maui.Controls` - MAUI framework --- ## License -Proprietary. See LICENSE file. +MIT License - see [LICENSE](LICENSE) file.