diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs index 1fe0c6ae..8091d7ff 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs @@ -32,6 +32,7 @@ public class AuthFunctionSideEffectTests private readonly Mock _tenantConfigMock; private readonly Mock _tenantAdminsMock; private readonly Mock _telegramMock; + private readonly Mock _opsEventsMock; private readonly Mock _globalNotificationMock; private readonly Mock _metricsRepoMock; private readonly Mock _autoApproveEnqueuerMock; @@ -96,6 +97,9 @@ public class AuthFunctionSideEffectTests _autoApproveEnqueuerMock = new Mock(); _signalR = new FakeSignalRNotificationService(); + _opsEventsMock = new Mock(null!, null!, null!) { CallBase = false }; + _opsEventsMock.Setup(x => x.RecordTenantSignupAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + _sut = new AuthFunction( Mock.Of>(), globalAdminMock.Object, @@ -115,7 +119,8 @@ public class AuthFunctionSideEffectTests _metricsRepoMock.Object, _tenantConfigMock.Object, Mock.Of>()) { CallBase = false }.Object, _signalR, adminConfigService.Object, - _offboardingRepo); + _offboardingRepo, + opsEvents: _opsEventsMock.Object); // Writes go through UpdateAsync / CreateOrUpdateAsync over one stored row (none by default: // a brand-new tenant). Default: all fire-and-forget calls succeed. @@ -234,6 +239,7 @@ public class AuthFunctionSideEffectTests Assert.Equal(CurrentDpa, written.DpaVersion); Assert.Equal("auth", source); _telegramMock.Verify(x => x.SendNewTenantSignupAsync(TenantId, Upn), Times.Once); + _opsEventsMock.Verify(x => x.RecordTenantSignupAsync(TenantId, Upn), Times.Once); // the routable record next to the ping _globalNotificationMock.Verify(x => x.CreateNotificationAsync( "preview_signup", "New Tenant Signup", It.Is(m => m.Contains(TenantId) && m.Contains("contoso.com") && m.Contains(Upn)), diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs index c7e713a0..287180b2 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs @@ -39,6 +39,7 @@ public class AuthFunction private readonly AdminConfigurationService _adminConfigService; private readonly IOffboardingAuditRepository _offboardingRepo; private readonly IPushDeviceRepository? _pushRepo; + private readonly OpsEventService? _opsEvents; public AuthFunction( ILogger logger, @@ -57,10 +58,12 @@ public class AuthFunction ISignalRNotificationService signalRService, AdminConfigurationService adminConfigService, IOffboardingAuditRepository offboardingRepo, - IPushDeviceRepository? pushRepo = null) + IPushDeviceRepository? pushRepo = null, + OpsEventService? opsEvents = null) { _logger = logger; _pushRepo = pushRepo; + _opsEvents = opsEvents; _offboardingRepo = offboardingRepo; _adminConfigService = adminConfigService; _identityResolver = identityResolver; @@ -470,6 +473,11 @@ public class AuthFunction // This login's view follows what was just stored. Seed(tenantConfig); + // The ops event is the routable record (alert rules → any channel, Push included); it is + // written before the response and never throws. The Telegram ping below stays until retired. + if (_opsEvents != null) + await _opsEvents.RecordTenantSignupAsync(tenantId, upn); + // Fire-and-forget: Telegram _ = _telegramNotificationService.SendNewTenantSignupAsync(tenantId, upn) .ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitDiagFilesReportFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitDiagFilesReportFunction.cs index e4e1b95f..917f3b2d 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitDiagFilesReportFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitDiagFilesReportFunction.cs @@ -24,19 +24,22 @@ namespace AutopilotMonitor.Functions.Functions.Reports private readonly IMaintenanceRepository _maintenanceRepo; private readonly TelegramNotificationService _telegramNotificationService; private readonly GlobalNotificationService _globalNotificationService; + private readonly OpsEventService? _opsEvents; public SubmitDiagFilesReportFunction( ILogger logger, SessionReportService sessionReportService, IMaintenanceRepository maintenanceRepo, TelegramNotificationService telegramNotificationService, - GlobalNotificationService globalNotificationService) + GlobalNotificationService globalNotificationService, + OpsEventService? opsEvents = null) { _logger = logger; _sessionReportService = sessionReportService; _maintenanceRepo = maintenanceRepo; _telegramNotificationService = telegramNotificationService; _globalNotificationService = globalNotificationService; + _opsEvents = opsEvents; } [Function("SubmitDiagFilesReport")] @@ -110,6 +113,10 @@ namespace AutopilotMonitor.Functions.Functions.Reports ); } + // Ops event (routable through alert rules, Push included) — written before the response, never throws + if (_opsEvents != null) + await _opsEvents.RecordDiagFilesReportSubmittedAsync(request.TenantId, userIdentifier, metadata.ReportId); + // Telegram notification — best effort _ = _telegramNotificationService.SendDiagFilesReportAsync( request.TenantId, userIdentifier, metadata.ReportId, request.Comment ?? string.Empty); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs index bf140d5d..db425e01 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Reports/SubmitSessionReportFunction.cs @@ -18,19 +18,22 @@ namespace AutopilotMonitor.Functions.Functions.Reports private readonly IMaintenanceRepository _maintenanceRepo; private readonly TelegramNotificationService _telegramNotificationService; private readonly GlobalNotificationService _globalNotificationService; + private readonly OpsEventService? _opsEvents; public SubmitSessionReportFunction( ILogger logger, SessionReportService sessionReportService, IMaintenanceRepository maintenanceRepo, TelegramNotificationService telegramNotificationService, - GlobalNotificationService globalNotificationService) + GlobalNotificationService globalNotificationService, + OpsEventService? opsEvents = null) { _logger = logger; _sessionReportService = sessionReportService; _maintenanceRepo = maintenanceRepo; _telegramNotificationService = telegramNotificationService; _globalNotificationService = globalNotificationService; + _opsEvents = opsEvents; } [Function("SubmitSessionReport")] @@ -119,6 +122,10 @@ namespace AutopilotMonitor.Functions.Functions.Reports ); } + // Ops event (routable through alert rules, Push included) — written before the response, never throws + if (_opsEvents != null) + await _opsEvents.RecordSessionReportSubmittedAsync(request.TenantId, userIdentifier, sessionId, metadata.ReportId); + // Telegram notification — best effort _ = _telegramNotificationService.SendSessionReportAsync( request.TenantId, userIdentifier, sessionId, metadata.ReportId, request.Comment ?? string.Empty); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Rules/Submissions/SubmitRuleSubmissionsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Rules/Submissions/SubmitRuleSubmissionsFunction.cs index eef2ab2c..c07e88a0 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Rules/Submissions/SubmitRuleSubmissionsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Rules/Submissions/SubmitRuleSubmissionsFunction.cs @@ -20,19 +20,22 @@ namespace AutopilotMonitor.Functions.Functions.Rules.Submissions private readonly IMaintenanceRepository _maintenanceRepo; private readonly TelegramNotificationService _telegram; private readonly GlobalNotificationService _globalNotifications; + private readonly OpsEventService? _opsEvents; public SubmitRuleSubmissionsFunction( ILogger logger, RuleSubmissionService service, IMaintenanceRepository maintenanceRepo, TelegramNotificationService telegram, - GlobalNotificationService globalNotifications) + GlobalNotificationService globalNotifications, + OpsEventService? opsEvents = null) { _logger = logger; _service = service; _maintenanceRepo = maintenanceRepo; _telegram = telegram; _globalNotifications = globalNotifications; + _opsEvents = opsEvents; } [Function("SubmitRuleSubmissions")] @@ -108,6 +111,9 @@ namespace AutopilotMonitor.Functions.Functions.Rules.Submissions // Operator notifications — best effort, one per rule so every id has its own deep link. foreach (var s in created) { + // Ops event (routable through alert rules, Push included) — written before the response, never throws + if (_opsEvents != null) + await _opsEvents.RecordRuleSubmissionReceivedAsync(request.TenantId, userIdentifier, s.SubmissionId, s.RuleKind, s.SourceRuleId); _ = _telegram.SendRuleSubmissionAsync(s.SubmissionId, request.TenantId, userIdentifier, s.RuleKind, s.SourceRuleId, s.Title, s.Comment ?? string.Empty); _ = _globalNotifications.CreateNotificationAsync( "rule_submission", diff --git a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs index a43bd8f7..b27ced7d 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs @@ -1181,6 +1181,32 @@ namespace AutopilotMonitor.Functions.Services null, "System.Push", new { host, scope = scopeKind }); + // ── Tenant: customer-initiated submissions (plan push-relay, Phase 0) ───── + // Info events next to the direct Telegram pings, so an alert rule can route them to any + // channel. Like FeedbackReceived, message and details carry ids only — never the submitter + // or a comment; the portal page behind portalPath has them. The user id column keeps the + // submitter for the admin view and never reaches a channel. + + public virtual Task RecordTenantSignupAsync(string tenantId, string upn) + => WriteAsync(OpsEventCategory.Tenant, OpsEventTypes.TenantSignup, OpsEventSeverity.Info, + "New tenant signed up", tenantId, upn, + new { portalPath = "/admin/tenants" }); + + public virtual Task RecordSessionReportSubmittedAsync(string tenantId, string submittedBy, string sessionId, string reportId) + => WriteAsync(OpsEventCategory.Tenant, OpsEventTypes.SessionReportSubmitted, OpsEventSeverity.Info, + "Session report submitted", tenantId, submittedBy, + new { sessionId, reportId, portalPath = $"/admin/reports/session-reports?reportId={Uri.EscapeDataString(reportId)}" }); + + public virtual Task RecordDiagFilesReportSubmittedAsync(string tenantId, string submittedBy, string reportId) + => WriteAsync(OpsEventCategory.Tenant, OpsEventTypes.DiagFilesReportSubmitted, OpsEventSeverity.Info, + "Diagnostic files submitted", tenantId, submittedBy, + new { reportId, portalPath = $"/admin/reports/session-reports?reportId={Uri.EscapeDataString(reportId)}" }); + + public virtual Task RecordRuleSubmissionReceivedAsync(string tenantId, string submittedBy, string submissionId, string ruleKind, string ruleId) + => WriteAsync(OpsEventCategory.Tenant, OpsEventTypes.RuleSubmissionReceived, OpsEventSeverity.Info, + "Rule submission received", tenantId, submittedBy, + new { submissionId, ruleKind, ruleId, portalPath = $"/admin/reports/rule-submissions?submissionId={Uri.EscapeDataString(submissionId)}" }); + // ── Core write method ────────────────────────────────────────────────── private async Task WriteAsync(string category, string eventType, string severity, diff --git a/src/McpServer/autopilot-monitor-mcp/src/generated/wire-vocabularies.generated.ts b/src/McpServer/autopilot-monitor-mcp/src/generated/wire-vocabularies.generated.ts index 368df48f..a79f29b5 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/generated/wire-vocabularies.generated.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/generated/wire-vocabularies.generated.ts @@ -106,6 +106,10 @@ export const OPS_EVENT_TYPES = [ "McpServicePrincipalFirstSeen", "OffboardingFeedbackReceived", "FeedbackReceived", + "TenantSignup", + "SessionReportSubmitted", + "DiagFilesReportSubmitted", + "RuleSubmissionReceived", "TenantOffboarded", "TenantOffboardingFailed", "TenantAutoApproved", diff --git a/src/Shared/AutopilotMonitor.Shared/DataAccess/OpsEventTypes.cs b/src/Shared/AutopilotMonitor.Shared/DataAccess/OpsEventTypes.cs index f7a92a78..3458919e 100644 --- a/src/Shared/AutopilotMonitor.Shared/DataAccess/OpsEventTypes.cs +++ b/src/Shared/AutopilotMonitor.Shared/DataAccess/OpsEventTypes.cs @@ -94,6 +94,18 @@ namespace AutopilotMonitor.Shared.DataAccess /// the portal (User Feedback), so a channel bound to this rule receives no personal data. /// public const string FeedbackReceived = "FeedbackReceived"; + /// + /// A new tenant signed up: its first sign-in seeded the domain (auth/me). Info. Recorded + /// next to the direct Telegram ping until that is retired (plan push-relay, Phase 0); message and + /// details carry no personal data — the Tenants page has the rest. + /// + public const string TenantSignup = "TenantSignup"; + /// A member submitted a session report. Info; details name the session and report id only. + public const string SessionReportSubmitted = "SessionReportSubmitted"; + /// A member submitted logs without a session (Submit Logs). Info; details name the report id only. + public const string DiagFilesReportSubmitted = "DiagFilesReportSubmitted"; + /// A member submitted a gather or analyze rule for review. Info; details name submission, kind and source rule id only. + public const string RuleSubmissionReceived = "RuleSubmissionReceived"; public const string TenantOffboarded = "TenantOffboarded"; public const string TenantOffboardingFailed = "TenantOffboardingFailed"; public const string TenantAutoApproved = "TenantAutoApproved"; @@ -168,7 +180,7 @@ namespace AutopilotMonitor.Shared.DataAccess ConsentFlowStarted, ConsentFlowSuccess, ConsentFlowFailed, ConsentRedirectUriMismatch, AppHomingFlipped, AppHomingFlippedWithEntraRoles, MaintenanceCompleted, MaintenanceFailed, MaintenanceLongRunning, MaintenanceStarted, MaintenanceSkippedLocked, SessionSweepCompleted, SessionSweepFailed, OpsEventCleanup, OrphanEventsCleaned, SessionDeletionMaintenanceStarted, SessionDeletionMaintenanceBudgetExceeded, SessionDeletionMaintenanceSkippedLocked, SessionDeletionMaintenanceLongRunning, SessionDeletionMaintenanceLongRunningSevere, SessionDeletionMaintenanceFailed, SessionDeletionStrandedQueued, SessionDeletionPoisoned, SessionDeletionMaintenanceCompleted, SessionDeletionMaintenanceFanoutSkipped, CriticalTableBackupCompleted, CriticalTableBackupPartial, CriticalTableBackupFailed, CriticalTableBackupSkippedLocked, BackupRowRestored, VerdictCalibrationDrift, DeviceBlocked, VersionBlocked, SessionTenantConflict, SessionOwnerMismatch, KillSignalDelivered, EmbeddedCertExpiringSoon, EmbeddedCertExpiringUrgent, EmbeddedCertExpired, EmbeddedCertBundleEmpty, SignalRConnectionsHigh, SignalRConnectionsCritical, SignalRMessagesHigh, SignalRMessagesCritical, PoisonQueueBacklogHigh, PoisonQueueBacklogCritical, ExcessiveSessionEventsAutoActioned, PrivilegedRouteDenied, McpServicePrincipalFirstSeen, - OffboardingFeedbackReceived, FeedbackReceived, TenantOffboarded, TenantOffboardingFailed, TenantAutoApproved, WelcomeEmailSent, WelcomeEmailSkipped, WelcomeEmailFailed, FarewellEmailSent, FarewellEmailSkipped, FarewellEmailFailed, TenantTrialStarted, TenantTrialExpiring, TenantTrialExpired, TenantPlanDowngraded, TenantRetentionGraceExpiring, TenantRetentionGraceEnded, RuleFrequencyRegression, AppVersionDurationRegression, CollectLogsQuickConfigEnabled, DiagnosticsUploadEnabled, DiagnosticsUploadDisabled, + OffboardingFeedbackReceived, FeedbackReceived, TenantSignup, SessionReportSubmitted, DiagFilesReportSubmitted, RuleSubmissionReceived, TenantOffboarded, TenantOffboardingFailed, TenantAutoApproved, WelcomeEmailSent, WelcomeEmailSkipped, WelcomeEmailFailed, FarewellEmailSent, FarewellEmailSkipped, FarewellEmailFailed, TenantTrialStarted, TenantTrialExpiring, TenantTrialExpired, TenantPlanDowngraded, TenantRetentionGraceExpiring, TenantRetentionGraceEnded, RuleFrequencyRegression, AppVersionDurationRegression, CollectLogsQuickConfigEnabled, DiagnosticsUploadEnabled, DiagnosticsUploadDisabled, SessionActionQueued, SessionTimeouts, AgentEmergencyBreak, AgentBinaryIntegrityMismatch, CmTraceTimeSkewRegression, ExcessiveSessionEvents, NewImeVersionDetected, ImePatternDriftSuspected, BlobStorageMissing, BlobStorageUnreachable, TelemetryItemsRejected, SlaBreachNotification, SlaConsecutiveFailures, SlaEvaluationCompleted, AzureMonitorAlert, PushDeliveryFailed, PushEndpointRefused, diff --git a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx index ac024e94..8358b4c4 100644 --- a/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx +++ b/src/Web/autopilot-monitor-web/app/admin/components/OpsAlertRulesSection.tsx @@ -157,6 +157,10 @@ const OPS_EVENT_TYPES: Record = { // FeedbackReceived — a member sent feedback from the help menu. Info-tier; carries no text // and no sender, the feedback itself is read under Reports → User Feedback. "FeedbackReceived", + "TenantSignup", + "SessionReportSubmitted", + "DiagFilesReportSubmitted", + "RuleSubmissionReceived", // TenantTrialStarted — the conversion moment, fired by PlanManagementFunction from BOTH // plan write paths (self-service POST trial and a GA grant via PATCH plan). Info-tier; // the payload carries domain, contact address and who granted it, so a sales/support diff --git a/src/Web/autopilot-monitor-web/utils/shared-manifests.generated.ts b/src/Web/autopilot-monitor-web/utils/shared-manifests.generated.ts index 2db3e8c7..06af4bbb 100644 --- a/src/Web/autopilot-monitor-web/utils/shared-manifests.generated.ts +++ b/src/Web/autopilot-monitor-web/utils/shared-manifests.generated.ts @@ -633,6 +633,10 @@ export const SHARED_MANIFEST = { "McpServicePrincipalFirstSeen", "OffboardingFeedbackReceived", "FeedbackReceived", + "TenantSignup", + "SessionReportSubmitted", + "DiagFilesReportSubmitted", + "RuleSubmissionReceived", "TenantOffboarded", "TenantOffboardingFailed", "TenantAutoApproved", diff --git a/src/Web/autopilot-monitor-web/utils/shared-manifests.json b/src/Web/autopilot-monitor-web/utils/shared-manifests.json index 4945e7f8..cf73ccbc 100644 --- a/src/Web/autopilot-monitor-web/utils/shared-manifests.json +++ b/src/Web/autopilot-monitor-web/utils/shared-manifests.json @@ -629,6 +629,10 @@ "McpServicePrincipalFirstSeen", "OffboardingFeedbackReceived", "FeedbackReceived", + "TenantSignup", + "SessionReportSubmitted", + "DiagFilesReportSubmitted", + "RuleSubmissionReceived", "TenantOffboarded", "TenantOffboardingFailed", "TenantAutoApproved",