mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
feat: added DO details to geo report
This commit is contained in:
@@ -1 +1 @@
|
||||
834
|
||||
835
|
||||
|
||||
+29
-2
@@ -117,6 +117,16 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
var totalDownloadSecs = locationApps.Sum(a => (double)a.DownloadDurationSeconds);
|
||||
var avgThroughput = totalDownloadSecs > 0 ? totalBytes / totalDownloadSecs : 0;
|
||||
|
||||
// Delivery Optimization metrics (apps with DO telemetry have DoDownloadMode >= 0)
|
||||
var doApps = locationApps.Where(a => a.DoDownloadMode >= 0).ToList();
|
||||
var doSessionCount = group
|
||||
.Where(s => appsBySession.ContainsKey(s.SessionId))
|
||||
.Count(s => appsBySession[s.SessionId].Any(a => a.DoDownloadMode >= 0));
|
||||
var totalDoPeers = doApps.Sum(a => a.DoBytesFromPeers);
|
||||
var totalDoHttp = doApps.Sum(a => a.DoBytesFromHttp);
|
||||
var totalDoBytes = totalDoPeers + totalDoHttp;
|
||||
var avgDoPct = totalDoBytes > 0 ? (double)totalDoPeers / totalDoBytes * 100 : 0;
|
||||
|
||||
locations.Add(new LocationMetrics
|
||||
{
|
||||
LocationKey = group.Key,
|
||||
@@ -134,7 +144,15 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
AvgAppCount = Math.Round(avgAppCount, 1),
|
||||
MinutesPerApp = Math.Round(minutesPerApp, 2),
|
||||
AvgThroughputBytesPerSec = Math.Round(avgThroughput, 0),
|
||||
TotalDownloadBytes = totalBytes
|
||||
TotalDownloadBytes = totalBytes,
|
||||
// Delivery Optimization
|
||||
DoSessionCount = doSessionCount,
|
||||
AvgDoPercentPeerCaching = Math.Round(avgDoPct, 1),
|
||||
TotalDoBytesFromPeers = totalDoPeers,
|
||||
TotalDoBytesFromHttp = totalDoHttp,
|
||||
TotalDoBytesFromLanPeers = doApps.Sum(a => a.DoBytesFromLanPeers),
|
||||
TotalDoBytesFromGroupPeers = doApps.Sum(a => a.DoBytesFromGroupPeers),
|
||||
TotalDoBytesFromInternetPeers = doApps.Sum(a => a.DoBytesFromInternetPeers)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,6 +174,12 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
.Select(l => l.AvgThroughputBytesPerSec).ToList();
|
||||
var globalAvgThroughput = allThroughputs.Count > 0 ? allThroughputs.Average() : 0;
|
||||
|
||||
// Global DO metrics (weighted by bytes, not simple average)
|
||||
var globalDoPeers = locations.Sum(l => l.TotalDoBytesFromPeers);
|
||||
var globalDoHttp = locations.Sum(l => l.TotalDoBytesFromHttp);
|
||||
var globalDoTotal = globalDoPeers + globalDoHttp;
|
||||
var globalDoPct = globalDoTotal > 0 ? (double)globalDoPeers / globalDoTotal * 100 : 0;
|
||||
|
||||
// Compute per-location comparisons and outlier detection
|
||||
foreach (var loc in locations)
|
||||
{
|
||||
@@ -190,7 +214,10 @@ namespace AutopilotMonitor.Functions.Functions.Metrics
|
||||
MedianDurationMinutes = Math.Round(globalMedianDuration, 1),
|
||||
AvgMinutesPerApp = Math.Round(globalAvgMinutesPerApp, 2),
|
||||
AvgThroughputBytesPerSec = Math.Round(globalAvgThroughput, 0),
|
||||
StdDevDurationMinutes = Math.Round(globalStdDev, 1)
|
||||
StdDevDurationMinutes = Math.Round(globalStdDev, 1),
|
||||
AvgDoPercentPeerCaching = Math.Round(globalDoPct, 1),
|
||||
TotalDoBytesFromPeers = globalDoPeers,
|
||||
TotalDoBytesFromHttp = globalDoHttp
|
||||
},
|
||||
ComputedAt = DateTime.UtcNow,
|
||||
TotalSessions = sessions.Count,
|
||||
|
||||
@@ -185,6 +185,23 @@ namespace AutopilotMonitor.Functions.Services
|
||||
if (existingDlBytes.HasValue && existingDlBytes.Value > 0)
|
||||
summary.DownloadBytes = existingDlBytes.Value;
|
||||
}
|
||||
|
||||
// Preserve DO telemetry from prior batch if current has none
|
||||
if (summary.DoBytesFromPeers == 0 && summary.DoDownloadMode == -1)
|
||||
{
|
||||
var existingDoPeers = existing.GetInt64("DoBytesFromPeers");
|
||||
if (existingDoPeers.HasValue && existingDoPeers.Value > 0)
|
||||
{
|
||||
summary.DoBytesFromPeers = existingDoPeers.Value;
|
||||
summary.DoBytesFromHttp = existing.GetInt64("DoBytesFromHttp") ?? 0;
|
||||
summary.DoPercentPeerCaching = existing.GetInt32("DoPercentPeerCaching") ?? 0;
|
||||
summary.DoDownloadMode = existing.GetInt32("DoDownloadMode") ?? -1;
|
||||
summary.DoDownloadDuration = existing.GetString("DoDownloadDuration") ?? string.Empty;
|
||||
summary.DoBytesFromLanPeers = existing.GetInt64("DoBytesFromLanPeers") ?? 0;
|
||||
summary.DoBytesFromGroupPeers = existing.GetInt64("DoBytesFromGroupPeers") ?? 0;
|
||||
summary.DoBytesFromInternetPeers = existing.GetInt64("DoBytesFromInternetPeers") ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var entity = new TableEntity(summary.TenantId, rowKey)
|
||||
@@ -199,7 +216,16 @@ namespace AutopilotMonitor.Functions.Services
|
||||
["FailureCode"] = summary.FailureCode ?? string.Empty,
|
||||
["FailureMessage"] = summary.FailureMessage ?? string.Empty,
|
||||
["StartedAt"] = EnsureUtc(summary.StartedAt),
|
||||
["CompletedAt"] = summary.CompletedAt.HasValue ? EnsureUtc(summary.CompletedAt.Value) : (DateTime?)null
|
||||
["CompletedAt"] = summary.CompletedAt.HasValue ? EnsureUtc(summary.CompletedAt.Value) : (DateTime?)null,
|
||||
// Delivery Optimization telemetry
|
||||
["DoBytesFromPeers"] = summary.DoBytesFromPeers,
|
||||
["DoBytesFromHttp"] = summary.DoBytesFromHttp,
|
||||
["DoPercentPeerCaching"] = summary.DoPercentPeerCaching,
|
||||
["DoDownloadMode"] = summary.DoDownloadMode,
|
||||
["DoDownloadDuration"] = summary.DoDownloadDuration ?? string.Empty,
|
||||
["DoBytesFromLanPeers"] = summary.DoBytesFromLanPeers,
|
||||
["DoBytesFromGroupPeers"] = summary.DoBytesFromGroupPeers,
|
||||
["DoBytesFromInternetPeers"] = summary.DoBytesFromInternetPeers
|
||||
};
|
||||
|
||||
await tableClient.UpsertEntityAsync(entity);
|
||||
@@ -278,7 +304,16 @@ namespace AutopilotMonitor.Functions.Services
|
||||
FailureCode = entity.GetString("FailureCode") ?? string.Empty,
|
||||
FailureMessage = entity.GetString("FailureMessage") ?? string.Empty,
|
||||
StartedAt = entity.GetDateTimeOffset("StartedAt")?.UtcDateTime ?? DateTime.MinValue,
|
||||
CompletedAt = entity.GetDateTimeOffset("CompletedAt")?.UtcDateTime
|
||||
CompletedAt = entity.GetDateTimeOffset("CompletedAt")?.UtcDateTime,
|
||||
// Delivery Optimization telemetry
|
||||
DoBytesFromPeers = entity.GetInt64("DoBytesFromPeers") ?? 0,
|
||||
DoBytesFromHttp = entity.GetInt64("DoBytesFromHttp") ?? 0,
|
||||
DoPercentPeerCaching = entity.GetInt32("DoPercentPeerCaching") ?? 0,
|
||||
DoDownloadMode = entity.GetInt32("DoDownloadMode") ?? -1,
|
||||
DoDownloadDuration = entity.GetString("DoDownloadDuration") ?? string.Empty,
|
||||
DoBytesFromLanPeers = entity.GetInt64("DoBytesFromLanPeers") ?? 0,
|
||||
DoBytesFromGroupPeers = entity.GetInt64("DoBytesFromGroupPeers") ?? 0,
|
||||
DoBytesFromInternetPeers = entity.GetInt64("DoBytesFromInternetPeers") ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,22 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public bool IsOutlier { get; set; }
|
||||
/// <summary>"fast", "slow", or null</summary>
|
||||
public string? OutlierDirection { get; set; }
|
||||
|
||||
// Delivery Optimization metrics
|
||||
/// <summary>Sessions at this location that have DO telemetry data</summary>
|
||||
public int DoSessionCount { get; set; }
|
||||
/// <summary>Weighted percentage of bytes from peers (0-100), computed from total peer/total DO bytes</summary>
|
||||
public double AvgDoPercentPeerCaching { get; set; }
|
||||
/// <summary>Total bytes downloaded from all peer sources</summary>
|
||||
public long TotalDoBytesFromPeers { get; set; }
|
||||
/// <summary>Total bytes downloaded from HTTP/CDN</summary>
|
||||
public long TotalDoBytesFromHttp { get; set; }
|
||||
/// <summary>Bytes from LAN peers</summary>
|
||||
public long TotalDoBytesFromLanPeers { get; set; }
|
||||
/// <summary>Bytes from group peers</summary>
|
||||
public long TotalDoBytesFromGroupPeers { get; set; }
|
||||
/// <summary>Bytes from internet peers</summary>
|
||||
public long TotalDoBytesFromInternetPeers { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,5 +85,11 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public double AvgMinutesPerApp { get; set; }
|
||||
public double AvgThroughputBytesPerSec { get; set; }
|
||||
public double StdDevDurationMinutes { get; set; }
|
||||
/// <summary>Global weighted average peer caching percentage</summary>
|
||||
public double AvgDoPercentPeerCaching { get; set; }
|
||||
/// <summary>Total peer bytes across all locations with DO data</summary>
|
||||
public long TotalDoBytesFromPeers { get; set; }
|
||||
/// <summary>Total HTTP bytes across all locations with DO data</summary>
|
||||
public long TotalDoBytesFromHttp { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ interface LocationMetric {
|
||||
throughputVsGlobalPct: number;
|
||||
isOutlier: boolean;
|
||||
outlierDirection: string | null;
|
||||
// Delivery Optimization
|
||||
doSessionCount: number;
|
||||
avgDoPercentPeerCaching: number;
|
||||
totalDoBytesFromPeers: number;
|
||||
totalDoBytesFromHttp: number;
|
||||
totalDoBytesFromLanPeers: number;
|
||||
totalDoBytesFromGroupPeers: number;
|
||||
totalDoBytesFromInternetPeers: number;
|
||||
}
|
||||
|
||||
interface GlobalAverages {
|
||||
@@ -43,6 +51,9 @@ interface GlobalAverages {
|
||||
avgMinutesPerApp: number;
|
||||
avgThroughputBytesPerSec: number;
|
||||
stdDevDurationMinutes: number;
|
||||
avgDoPercentPeerCaching: number;
|
||||
totalDoBytesFromPeers: number;
|
||||
totalDoBytesFromHttp: number;
|
||||
}
|
||||
|
||||
interface GeographicMetricsResponse {
|
||||
@@ -56,7 +67,7 @@ interface GeographicMetricsResponse {
|
||||
}
|
||||
|
||||
type GroupBy = "city" | "region" | "country";
|
||||
type SortBy = "sessionCount" | "avgDurationMinutes" | "appLoadScore" | "avgThroughputBytesPerSec";
|
||||
type SortBy = "sessionCount" | "avgDurationMinutes" | "appLoadScore" | "avgThroughputBytesPerSec" | "avgDoPercentPeerCaching";
|
||||
type TimeRange = "7d" | "30d" | "90d";
|
||||
|
||||
const durationColor = (value: number, globalAvg: number) => {
|
||||
@@ -283,7 +294,7 @@ export default function GeographicPerformancePage() {
|
||||
|
||||
{/* Summary Cards */}
|
||||
{geoMetrics && stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">Locations Tracked</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{geoMetrics.locations.length}</div>
|
||||
@@ -314,6 +325,22 @@ export default function GeographicPerformancePage() {
|
||||
{stats.slowest ? `${Math.round(stats.slowest.avgDurationMinutes)} min avg` : "No data"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">DO Peer Efficiency</div>
|
||||
<div className={`text-2xl font-bold ${
|
||||
geoMetrics.globalAverages.avgDoPercentPeerCaching > 30 ? "text-green-600" :
|
||||
geoMetrics.globalAverages.avgDoPercentPeerCaching > 0 ? "text-yellow-600" : "text-gray-400"
|
||||
}`}>
|
||||
{geoMetrics.globalAverages.avgDoPercentPeerCaching > 0
|
||||
? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}%`
|
||||
: "—"}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
{geoMetrics.globalAverages.totalDoBytesFromPeers > 0
|
||||
? `${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers)} from peers / ${formatBytes(geoMetrics.globalAverages.totalDoBytesFromPeers + geoMetrics.globalAverages.totalDoBytesFromHttp)} total`
|
||||
: "No DO data yet"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -338,7 +365,7 @@ export default function GeographicPerformancePage() {
|
||||
{geoMetrics && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div className="text-sm font-medium text-blue-800 mb-2">Global Averages (Benchmark)</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-blue-600 font-medium">Avg Duration:</span>{" "}
|
||||
<span className="text-blue-900">{geoMetrics.globalAverages.avgDurationMinutes} min</span>
|
||||
@@ -355,6 +382,14 @@ export default function GeographicPerformancePage() {
|
||||
<span className="text-blue-600 font-medium">Avg Throughput:</span>{" "}
|
||||
<span className="text-blue-900">{formatThroughput(geoMetrics.globalAverages.avgThroughputBytesPerSec)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-blue-600 font-medium">DO P2P:</span>{" "}
|
||||
<span className="text-blue-900">
|
||||
{geoMetrics.globalAverages.avgDoPercentPeerCaching > 0
|
||||
? `${geoMetrics.globalAverages.avgDoPercentPeerCaching}% peers`
|
||||
: "No data"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -407,6 +442,12 @@ export default function GeographicPerformancePage() {
|
||||
>
|
||||
Throughput <SortIcon col="avgThroughputBytesPerSec" />
|
||||
</th>
|
||||
<th
|
||||
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700"
|
||||
onClick={() => handleSort("avgDoPercentPeerCaching")}
|
||||
>
|
||||
P2P % <SortIcon col="avgDoPercentPeerCaching" />
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
vs Global
|
||||
</th>
|
||||
@@ -487,6 +528,28 @@ export default function GeographicPerformancePage() {
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
{formatThroughput(loc.avgThroughputBytesPerSec)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
{loc.doSessionCount > 0 ? (
|
||||
<div title={
|
||||
loc.totalDoBytesFromPeers > 0
|
||||
? `LAN: ${formatBytes(loc.totalDoBytesFromLanPeers)} | Group: ${formatBytes(loc.totalDoBytesFromGroupPeers)} | Internet: ${formatBytes(loc.totalDoBytesFromInternetPeers)}`
|
||||
: `${loc.doSessionCount} session(s) with DO data`
|
||||
}>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
loc.avgDoPercentPeerCaching >= 50 ? "bg-green-100 text-green-800" :
|
||||
loc.avgDoPercentPeerCaching >= 10 ? "bg-yellow-100 text-yellow-800" :
|
||||
"bg-gray-100 text-gray-600"
|
||||
}`}>
|
||||
{loc.avgDoPercentPeerCaching > 0 ? `${loc.avgDoPercentPeerCaching}%` : "0%"}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
({loc.doSessionCount})
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
{loc.durationVsGlobalPct !== 0 ? (
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user