mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
reports: snapshot session annotations into the report ZIP
SubmitReportAsync adds annotations.json (all lanes) to the package and annotationCount to report-metadata.json. All lanes are safe: report blobs are readable only through the GA-only session-reports routes. Fail-soft like the tenant-context snapshot; the copy outlives the session=s retention/delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,10 @@ in the tenant-visible audit log — same convention as GA session-report submiss
|
||||
never consume page budget.
|
||||
* **MCP write**: `annotate_session` (`strictGa`-gated, MUTATING) always PUTs the `globaladmin`
|
||||
lane — no lane argument by design. This is the frictionless labeling path after a session-debug.
|
||||
* **Session reports**: `SessionReportService.SubmitReportAsync` snapshots ALL lanes into
|
||||
`annotations.json` inside the report ZIP (`annotationCount` in report-metadata.json; fail-soft).
|
||||
Safe because report blobs are readable only through the GA-only session-reports routes, and
|
||||
valuable because the ZIP outlives the session's retention/delete.
|
||||
|
||||
# Citations
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
private readonly SessionReportDiagnosticsArchiveCopier _diagnosticsArchiveCopier;
|
||||
private readonly TenantConfigurationService _configService;
|
||||
private readonly IRuleRepository _ruleRepo;
|
||||
private readonly ISessionAnnotationRepository _annotationRepo;
|
||||
private const string ContainerName = "session-reports";
|
||||
|
||||
public SessionReportService(
|
||||
@@ -38,6 +39,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
SessionReportDiagnosticsArchiveCopier diagnosticsArchiveCopier,
|
||||
TenantConfigurationService configService,
|
||||
IRuleRepository ruleRepo,
|
||||
ISessionAnnotationRepository annotationRepo,
|
||||
ILogger<SessionReportService> logger)
|
||||
{
|
||||
_notificationRepo = notificationRepo;
|
||||
@@ -47,6 +49,7 @@ namespace AutopilotMonitor.Functions.Services
|
||||
_diagnosticsArchiveCopier = diagnosticsArchiveCopier;
|
||||
_configService = configService;
|
||||
_ruleRepo = ruleRepo;
|
||||
_annotationRepo = annotationRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,6 +98,13 @@ namespace AutopilotMonitor.Functions.Services
|
||||
// have changed by the time an operator investigates). Fail-soft; never blocks.
|
||||
var tenantContext = await CollectTenantContextAsync(request.TenantId, reportId);
|
||||
|
||||
// Human annotations at report time — the submitter's own verdict/notes are exactly
|
||||
// the context an investigating operator wants first, and the snapshot survives the
|
||||
// session's retention/delete. ALL lanes are included: the report blob is readable
|
||||
// only through the GA-only session-reports routes, so the platform-internal
|
||||
// globaladmin lane never reaches tenant callers this way. Fail-soft; never blocks.
|
||||
var annotations = await CollectAnnotationsAsync(request.TenantId, request.SessionId, reportId);
|
||||
|
||||
// Decode attachments up front so report-metadata.json can record their outcome
|
||||
// instead of silently dropping invalid payloads.
|
||||
var (screenshotBytes, screenshotStatus) = DecodeAttachment(request.ScreenshotBase64, reportId, "screenshot");
|
||||
@@ -128,6 +138,12 @@ namespace AutopilotMonitor.Functions.Services
|
||||
AddTextEntry(archive, "timeline.txt", request.TimelineExportTxt);
|
||||
}
|
||||
|
||||
// Human annotations snapshot (all lanes) — omitted when none exist.
|
||||
if (annotations is { Count: > 0 })
|
||||
{
|
||||
AddJsonEntry(archive, "annotations.json", annotations);
|
||||
}
|
||||
|
||||
// Report metadata
|
||||
AddJsonEntry(archive, "report-metadata.json", new
|
||||
{
|
||||
@@ -157,6 +173,9 @@ namespace AutopilotMonitor.Functions.Services
|
||||
screenshot = screenshotStatus,
|
||||
agentLog = agentLogStatus
|
||||
},
|
||||
// Count only — the rows themselves live in annotations.json. 0 with a
|
||||
// missing annotations.json ⇒ either none existed or the read failed soft.
|
||||
annotationCount = annotations?.Count ?? 0,
|
||||
tenantContext
|
||||
});
|
||||
|
||||
@@ -360,6 +379,35 @@ namespace AutopilotMonitor.Functions.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the session's human annotations (all lanes) for annotations.json.
|
||||
/// Fail-soft: a failed read yields null and never blocks the report.
|
||||
/// </summary>
|
||||
private async Task<List<object>?> CollectAnnotationsAsync(string tenantId, string sessionId, string reportId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rows = await _annotationRepo.GetForSessionAsync(tenantId, sessionId);
|
||||
return rows.Select(a => (object)new
|
||||
{
|
||||
lane = a.Lane,
|
||||
verdict = a.Verdict,
|
||||
note = a.Note,
|
||||
authorUpn = a.AuthorUpn,
|
||||
authorDisplayName = a.AuthorDisplayName,
|
||||
createdByUpn = a.CreatedByUpn,
|
||||
createdAtUtc = a.CreatedAtUtc,
|
||||
updatedAtUtc = a.UpdatedAtUtc,
|
||||
ruleIds = a.RuleIds,
|
||||
}).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Report {ReportId}: annotations snapshot failed — omitted from package", reportId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes an optional base64 attachment. Returns the bytes plus a status string
|
||||
/// ("none" | "included" | "invalid-base64") that is persisted in report-metadata.json —
|
||||
|
||||
Reference in New Issue
Block a user