feat: added DO details to geo report

This commit is contained in:
Oliver Kieselbach
2026-04-05 00:16:39 +02:00
parent ab0c6b5092
commit c7227df06d
6 changed files with 156 additions and 9 deletions
@@ -1 +1 @@
834
835
@@ -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