diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs
new file mode 100644
index 00000000..4585b575
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/NetworkChangeDetector.cs
@@ -0,0 +1,594 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.Http;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using AutopilotMonitor.Agent.Core.Logging;
+using AutopilotMonitor.Shared;
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
+{
+ ///
+ /// Detects network changes (SSID switches, wired↔wireless, IP changes) during provisioning
+ /// and runs MDM endpoint connectivity checks after each change.
+ /// Always-on collector — fires network_state_change and network_connectivity_check events.
+ ///
+ public class NetworkChangeDetector : IDisposable
+ {
+ private readonly string _sessionId;
+ private readonly string _tenantId;
+ private readonly Action _onEventCollected;
+ private readonly AgentLogger _logger;
+ private readonly string _apiBaseUrl;
+
+ private NetworkStateSnapshot _lastKnownState;
+ private Timer _debounceTimer;
+ private HttpClient _connectivityCheckClient;
+ private readonly object _stateLock = new object();
+ private bool _disposed;
+
+ private const int DebounceDelayMs = 3000;
+ private const int ConnectivityCheckDelayMs = 5000;
+ private const int ConnectivityCheckTimeoutMs = 8000;
+ private const int WifiCollectTimeoutMs = 3000;
+ private const string Source = "NetworkChangeDetector";
+
+ ///
+ /// Well-known MDM/Entra endpoints that must be reachable for enrollment to succeed.
+ ///
+ private static readonly (string Name, string Url)[] MdmEndpoints =
+ {
+ ("Entra ID", "https://login.microsoftonline.com"),
+ ("Device Registration", "https://enterpriseregistration.windows.net"),
+ ("Intune Portal", "https://portal.manage.microsoft.com"),
+ ("Microsoft Graph", "https://graph.microsoft.com"),
+ };
+
+ public NetworkChangeDetector(
+ string sessionId,
+ string tenantId,
+ Action onEventCollected,
+ AgentLogger logger,
+ string apiBaseUrl = null)
+ {
+ _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
+ _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId));
+ _onEventCollected = onEventCollected ?? throw new ArgumentNullException(nameof(onEventCollected));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _apiBaseUrl = apiBaseUrl;
+ }
+
+ public void Start()
+ {
+ _logger.Info("NetworkChangeDetector: starting (monitoring network changes)");
+
+ _connectivityCheckClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromMilliseconds(ConnectivityCheckTimeoutMs)
+ };
+
+ // Capture initial network state as baseline
+ _lastKnownState = CaptureCurrentState();
+ _logger.Info($"NetworkChangeDetector: initial state — {FormatSnapshotSummary(_lastKnownState)}");
+
+ NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
+ }
+
+ public void Stop()
+ {
+ NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
+
+ lock (_stateLock)
+ {
+ _debounceTimer?.Dispose();
+ _debounceTimer = null;
+ }
+
+ _connectivityCheckClient?.Dispose();
+ _connectivityCheckClient = null;
+
+ _logger.Info("NetworkChangeDetector: stopped");
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ Stop();
+ }
+
+ // -------------------------------------------------------------------
+ // Event handling & debounce
+ // -------------------------------------------------------------------
+
+ private void OnNetworkAddressChanged(object sender, EventArgs e)
+ {
+ if (_disposed) return;
+
+ lock (_stateLock)
+ {
+ // Reset debounce timer — collapses rapid OS events into one logical change
+ _debounceTimer?.Dispose();
+ _debounceTimer = new Timer(
+ OnDebounceElapsed,
+ null,
+ DebounceDelayMs,
+ Timeout.Infinite);
+ }
+ }
+
+ private void OnDebounceElapsed(object state)
+ {
+ if (_disposed) return;
+
+ Task.Run(() =>
+ {
+ try
+ {
+ ProcessNetworkChange();
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning($"NetworkChangeDetector: error processing network change: {ex.Message}");
+ }
+ });
+ }
+
+ // -------------------------------------------------------------------
+ // Core logic
+ // -------------------------------------------------------------------
+
+ private void ProcessNetworkChange()
+ {
+ if (_disposed) return;
+
+ var newState = CaptureCurrentState();
+
+ NetworkStateSnapshot previousState;
+ lock (_stateLock)
+ {
+ previousState = _lastKnownState;
+ _lastKnownState = newState;
+ }
+
+ // Suppress events when network hasn't actually changed (DHCP renewals, OS chatter)
+ if (previousState != null && previousState.IsSameNetwork(newState))
+ {
+ _logger.Debug("NetworkChangeDetector: network address changed but same logical network — suppressing");
+ return;
+ }
+
+ _logger.Info($"NetworkChangeDetector: network change detected — {FormatSnapshotSummary(previousState)} → {FormatSnapshotSummary(newState)}");
+
+ // Emit network_state_change event
+ var changeType = ClassifyChange(previousState, newState);
+ var message = BuildChangeMessage(previousState, newState);
+
+ var data = new Dictionary
+ {
+ // Before state
+ { "before_connectionType", previousState?.ConnectionType ?? "None" },
+ { "before_adapterName", previousState?.AdapterName ?? "None" },
+ { "before_ipAddress", previousState?.IpAddress ?? "None" },
+ { "before_gateway", previousState?.Gateway ?? "None" },
+ { "before_wifiSsid", previousState?.WifiSsid ?? "n/a" },
+ { "before_linkSpeedMbps", previousState?.LinkSpeedMbps ?? 0 },
+
+ // After state
+ { "after_connectionType", newState.ConnectionType ?? "None" },
+ { "after_adapterName", newState.AdapterName ?? "None" },
+ { "after_ipAddress", newState.IpAddress ?? "None" },
+ { "after_gateway", newState.Gateway ?? "None" },
+ { "after_wifiSsid", newState.WifiSsid ?? "n/a" },
+ { "after_linkSpeedMbps", newState.LinkSpeedMbps },
+
+ // Change classification
+ { "changeType", changeType },
+ { "hadNetwork", previousState?.HasNetwork ?? false },
+ { "hasNetwork", newState.HasNetwork }
+ };
+
+ _onEventCollected(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = Constants.EventTypes.NetworkStateChange,
+ Severity = EventSeverity.Warning,
+ Source = Source,
+ Phase = EnrollmentPhase.Unknown,
+ Message = message,
+ Data = data
+ });
+
+ // Wait for new connection to stabilize, then check MDM endpoint connectivity
+ try
+ {
+ Task.Delay(ConnectivityCheckDelayMs).Wait();
+ if (!_disposed)
+ {
+ RunConnectivityChecksAsync().Wait();
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Debug($"NetworkChangeDetector: connectivity check failed: {ex.Message}");
+ }
+ }
+
+ private async Task RunConnectivityChecksAsync()
+ {
+ if (_disposed || _connectivityCheckClient == null) return;
+
+ // Build endpoint list: static MDM endpoints + dynamic backend URL
+ var endpoints = new List<(string Name, string Url)>(MdmEndpoints);
+ if (!string.IsNullOrEmpty(_apiBaseUrl))
+ {
+ endpoints.Add(("Autopilot Monitor API", _apiBaseUrl));
+ }
+
+ // Run all checks in parallel
+ var tasks = endpoints.Select(ep => CheckEndpointAsync(ep.Name, ep.Url)).ToArray();
+
+ Dictionary[] results;
+ try
+ {
+ results = await Task.WhenAll(tasks);
+ }
+ catch (Exception ex)
+ {
+ _logger.Debug($"NetworkChangeDetector: connectivity check tasks failed: {ex.Message}");
+ return;
+ }
+
+ if (_disposed) return;
+
+ var reachableCount = results.Count(r => r.ContainsKey("reachable") && (bool)r["reachable"]);
+ var totalCount = results.Length;
+ var allReachable = reachableCount == totalCount;
+
+ _onEventCollected(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = Constants.EventTypes.NetworkConnectivityCheck,
+ Severity = allReachable ? EventSeverity.Info : EventSeverity.Warning,
+ Source = Source,
+ Phase = EnrollmentPhase.Unknown,
+ Message = $"Connectivity check after network change: {reachableCount}/{totalCount} endpoints reachable",
+ Data = new Dictionary
+ {
+ { "reachableCount", reachableCount },
+ { "totalCount", totalCount },
+ { "allReachable", allReachable },
+ { "checkDelayMs", ConnectivityCheckDelayMs },
+ { "timeoutMs", ConnectivityCheckTimeoutMs },
+ { "results", results.ToList() }
+ }
+ });
+
+ if (allReachable)
+ _logger.Info($"NetworkChangeDetector: connectivity check passed ({reachableCount}/{totalCount} reachable)");
+ else
+ _logger.Warning($"NetworkChangeDetector: connectivity check partial — {reachableCount}/{totalCount} endpoints reachable");
+ }
+
+ private async Task> CheckEndpointAsync(string name, string url)
+ {
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ // Use GET — any HTTP response (even 401/403) means the network path works
+ var response = await _connectivityCheckClient.GetAsync(url);
+ sw.Stop();
+ return new Dictionary
+ {
+ { "endpoint", name },
+ { "url", url },
+ { "reachable", true },
+ { "httpStatus", (int)response.StatusCode },
+ { "latencyMs", sw.ElapsedMilliseconds },
+ { "error", (object)null }
+ };
+ }
+ catch (Exception ex)
+ {
+ sw.Stop();
+ // Unwrap AggregateException from async
+ var innerEx = ex is AggregateException agg ? agg.InnerException ?? ex : ex;
+ return new Dictionary
+ {
+ { "endpoint", name },
+ { "url", url },
+ { "reachable", false },
+ { "httpStatus", (object)null },
+ { "latencyMs", sw.ElapsedMilliseconds },
+ { "error", $"{innerEx.GetType().Name}: {innerEx.Message}" }
+ };
+ }
+ }
+
+ // -------------------------------------------------------------------
+ // State capture
+ // -------------------------------------------------------------------
+
+ private NetworkStateSnapshot CaptureCurrentState()
+ {
+ try
+ {
+ var activeNic = FindActiveNetworkInterface();
+ if (activeNic == null)
+ {
+ return new NetworkStateSnapshot { HasNetwork = false, CapturedAt = DateTime.UtcNow };
+ }
+
+ var isWifi = activeNic.NetworkInterfaceType == NetworkInterfaceType.Wireless80211;
+ var ipProps = activeNic.GetIPProperties();
+
+ // First IPv4 unicast address
+ string ipAddress = null;
+ foreach (var addr in ipProps.UnicastAddresses)
+ {
+ if (addr.Address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ ipAddress = addr.Address.ToString();
+ break;
+ }
+ }
+
+ // First non-0.0.0.0 gateway
+ string gateway = null;
+ foreach (var gw in ipProps.GatewayAddresses)
+ {
+ if (gw.Address.ToString() != "0.0.0.0")
+ {
+ gateway = gw.Address.ToString();
+ break;
+ }
+ }
+
+ var snapshot = new NetworkStateSnapshot
+ {
+ HasNetwork = true,
+ AdapterName = activeNic.Name,
+ AdapterDescription = activeNic.Description,
+ InterfaceType = activeNic.NetworkInterfaceType.ToString(),
+ ConnectionType = isWifi ? "WiFi" : "Ethernet",
+ MacAddress = activeNic.GetPhysicalAddress().ToString(),
+ InterfaceId = activeNic.Id,
+ LinkSpeedMbps = activeNic.Speed / 1_000_000,
+ IpAddress = ipAddress,
+ Gateway = gateway,
+ CapturedAt = DateTime.UtcNow
+ };
+
+ // Collect WiFi SSID synchronously (needed for before/after comparison)
+ if (isWifi)
+ {
+ var wifiInfo = CollectWiFiInfo();
+ snapshot.WifiSsid = wifiInfo.ssid;
+ snapshot.WifiSignalPercent = wifiInfo.signal;
+ snapshot.WifiRadioType = wifiInfo.radioType;
+ }
+
+ return snapshot;
+ }
+ catch (Exception ex)
+ {
+ _logger.Debug($"NetworkChangeDetector: state capture failed: {ex.Message}");
+ return new NetworkStateSnapshot { HasNetwork = false, CapturedAt = DateTime.UtcNow };
+ }
+ }
+
+ ///
+ /// Collects WiFi SSID, signal, and radio type via netsh wlan show interfaces.
+ /// Synchronous with timeout — needed before emitting the event.
+ ///
+ private (string ssid, int? signal, string radioType) CollectWiFiInfo()
+ {
+ string ssid = null;
+ int? signal = null;
+ string radioType = null;
+
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = "netsh.exe",
+ Arguments = "wlan show interfaces",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ string output;
+ using (var process = Process.Start(psi))
+ {
+ output = process.StandardOutput.ReadToEnd();
+ if (!process.WaitForExit(WifiCollectTimeoutMs))
+ {
+ try { process.Kill(); } catch { }
+ _logger.Debug("NetworkChangeDetector: netsh timed out");
+ return (null, null, null);
+ }
+ }
+
+ if (string.IsNullOrEmpty(output))
+ return (null, null, null);
+
+ foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var trimmed = line.Trim();
+ var colonIndex = trimmed.IndexOf(':');
+ if (colonIndex < 0) continue;
+
+ var key = trimmed.Substring(0, colonIndex).Trim();
+ var value = trimmed.Substring(colonIndex + 1).Trim();
+
+ if (key.Equals("SSID", StringComparison.OrdinalIgnoreCase) &&
+ !key.Equals("BSSID", StringComparison.OrdinalIgnoreCase))
+ {
+ ssid = value;
+ }
+ else if (key.Equals("Signal", StringComparison.OrdinalIgnoreCase))
+ {
+ if (int.TryParse(value.TrimEnd('%'), out var sig))
+ signal = sig;
+ }
+ else if (key.Equals("Radio type", StringComparison.OrdinalIgnoreCase))
+ {
+ radioType = value;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Debug($"NetworkChangeDetector: WiFi info collection failed: {ex.Message}");
+ }
+
+ return (ssid, signal, radioType);
+ }
+
+ // -------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------
+
+ ///
+ /// Finds the active network interface: Up, not Loopback/Tunnel, has a non-0.0.0.0 gateway.
+ ///
+ private static NetworkInterface FindActiveNetworkInterface()
+ {
+ try
+ {
+ var interfaces = NetworkInterface.GetAllNetworkInterfaces();
+ foreach (var nic in interfaces)
+ {
+ if (nic.OperationalStatus != OperationalStatus.Up)
+ continue;
+ if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback ||
+ nic.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
+ continue;
+
+ var ipProps = nic.GetIPProperties();
+ foreach (var gw in ipProps.GatewayAddresses)
+ {
+ if (gw.Address.ToString() != "0.0.0.0")
+ return nic;
+ }
+ }
+ }
+ catch
+ {
+ // Caller handles null
+ }
+ return null;
+ }
+
+ private static string BuildChangeMessage(NetworkStateSnapshot before, NetworkStateSnapshot after)
+ {
+ var beforeDesc = FormatSnapshotBrief(before);
+ var afterDesc = FormatSnapshotBrief(after);
+
+ if (before == null || !before.HasNetwork)
+ {
+ if (after.HasNetwork)
+ return $"Network connected: {afterDesc}";
+ return "Network state changed (no connectivity)";
+ }
+
+ if (!after.HasNetwork)
+ return $"Network lost (was {beforeDesc})";
+
+ return $"Network changed: {beforeDesc} \u2192 {afterDesc}";
+ }
+
+ private static string FormatSnapshotBrief(NetworkStateSnapshot snapshot)
+ {
+ if (snapshot == null || !snapshot.HasNetwork)
+ return "None";
+
+ if (snapshot.ConnectionType == "WiFi" && !string.IsNullOrEmpty(snapshot.WifiSsid))
+ return $"WiFi '{snapshot.WifiSsid}'";
+
+ return $"{snapshot.ConnectionType} ({snapshot.IpAddress ?? "no IP"})";
+ }
+
+ private static string FormatSnapshotSummary(NetworkStateSnapshot snapshot)
+ {
+ if (snapshot == null || !snapshot.HasNetwork)
+ return "no network";
+
+ var summary = $"{snapshot.ConnectionType}";
+ if (!string.IsNullOrEmpty(snapshot.WifiSsid))
+ summary += $" SSID='{snapshot.WifiSsid}'";
+ if (!string.IsNullOrEmpty(snapshot.IpAddress))
+ summary += $" IP={snapshot.IpAddress}";
+ return summary;
+ }
+
+ private static string ClassifyChange(NetworkStateSnapshot before, NetworkStateSnapshot after)
+ {
+ if (before == null || !before.HasNetwork)
+ return after.HasNetwork ? "network_restored" : "no_change";
+
+ if (!after.HasNetwork)
+ return "network_lost";
+
+ // Both have network — classify the change
+ if (before.ConnectionType != after.ConnectionType)
+ return "type_change";
+
+ if (before.ConnectionType == "WiFi" && after.ConnectionType == "WiFi" &&
+ before.WifiSsid != after.WifiSsid)
+ return "ssid_change";
+
+ if (before.InterfaceId != after.InterfaceId)
+ return "adapter_change";
+
+ return "ip_change";
+ }
+
+ // -------------------------------------------------------------------
+ // Inner type: network state snapshot
+ // -------------------------------------------------------------------
+
+ private class NetworkStateSnapshot
+ {
+ public bool HasNetwork { get; set; }
+ public string AdapterName { get; set; }
+ public string AdapterDescription { get; set; }
+ public string InterfaceType { get; set; }
+ public string ConnectionType { get; set; }
+ public string MacAddress { get; set; }
+ public string InterfaceId { get; set; }
+ public long LinkSpeedMbps { get; set; }
+ public string IpAddress { get; set; }
+ public string Gateway { get; set; }
+ public string WifiSsid { get; set; }
+ public int? WifiSignalPercent { get; set; }
+ public string WifiRadioType { get; set; }
+ public DateTime CapturedAt { get; set; }
+
+ ///
+ /// Returns true if both snapshots represent the same logical network connection.
+ /// Suppresses events for DHCP renewals and other OS chatter.
+ ///
+ public bool IsSameNetwork(NetworkStateSnapshot other)
+ {
+ if (other == null) return false;
+ if (HasNetwork != other.HasNetwork) return false;
+ if (!HasNetwork && !other.HasNetwork) return true;
+
+ // Same adapter + same IP + same SSID = same logical network
+ return InterfaceId == other.InterfaceId &&
+ IpAddress == other.IpAddress &&
+ WifiSsid == other.WifiSsid;
+ }
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
index c3dc0369..abf5e213 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
@@ -114,6 +114,17 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_desktopArrivalDetector.Start();
_logger.Info("DesktopArrivalDetector started — monitoring for real user desktop");
+ // Network change detector — monitors for NIC/SSID/IP changes during provisioning
+ _networkChangeDetector = new NetworkChangeDetector(
+ _configuration.SessionId,
+ _configuration.TenantId,
+ EmitEvent,
+ _logger,
+ _configuration.ApiBaseUrl
+ );
+ _networkChangeDetector.Start();
+ _logger.Info("NetworkChangeDetector started — monitoring for network changes");
+
// Agent max lifetime safety net — prevents zombie agents
var maxLifetimeMinutes = collectors?.AgentMaxLifetimeMinutes ?? _configuration.AgentMaxLifetimeMinutes;
if (maxLifetimeMinutes > 0)
@@ -154,6 +165,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_desktopArrivalDetector = null;
}
+ _networkChangeDetector?.Dispose();
+ _networkChangeDetector = null;
+
_performanceCollector?.Stop();
_performanceCollector?.Dispose();
_performanceCollector = null;
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
index 6311cd28..b958d246 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
@@ -726,6 +726,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_performanceCollector?.Dispose();
_agentSelfMetricsCollector?.Dispose();
_enrollmentTracker?.Dispose();
+ _networkChangeDetector?.Dispose();
_gatherRuleExecutor?.Dispose();
_remoteConfigService?.Dispose();
_completionEvent.Dispose();
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs
index 0317b950..64da7675 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.cs
@@ -68,6 +68,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// Desktop arrival detection (no-ESP scenarios)
private DesktopArrivalDetector _desktopArrivalDetector;
+ // Network change detection (always on)
+ private NetworkChangeDetector _networkChangeDetector;
+
// Agent max lifetime safety net
private Timer _maxLifetimeTimer;
private readonly DateTime _agentStartTimeUtc = DateTime.UtcNow;
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index 4619a8a0..534b992c 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-451
+453
diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs
index 776d6016..b3c57087 100644
--- a/src/Shared/AutopilotMonitor.Shared/Constants.cs
+++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs
@@ -134,6 +134,7 @@ namespace AutopilotMonitor.Shared
public const string AppDownloadStarted = "app_download_started";
public const string AppInstallSkipped = "app_install_skipped";
public const string NetworkStateChange = "network_state_change";
+ public const string NetworkConnectivityCheck = "network_connectivity_check";
public const string ErrorDetected = "error_detected";
public const string PerformanceSnapshot = "performance_snapshot";
public const string LogEntry = "log_entry";