@oneuptime/common 11.7.4 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/ExceptionInstance.ts +67 -0
- package/Models/AnalyticsModels/Index.ts +10 -0
- package/Models/AnalyticsModels/Log.ts +60 -0
- package/Models/AnalyticsModels/RumSession.ts +1401 -0
- package/Models/AnalyticsModels/RumSessionChunk.ts +798 -0
- package/Models/AnalyticsModels/Span.ts +60 -0
- package/Models/DatabaseModels/AIAgentTaskPullRequest.ts +52 -0
- package/Models/DatabaseModels/AIRun.ts +31 -0
- package/Models/DatabaseModels/Alert.ts +29 -0
- package/Models/DatabaseModels/AlertCustomField.ts +1 -1
- package/Models/DatabaseModels/AlertFeed.ts +1 -0
- package/Models/DatabaseModels/AutoRemediationRule.ts +989 -0
- package/Models/DatabaseModels/AutoRemediationSuggestion.ts +874 -0
- package/Models/DatabaseModels/CodeRepository.ts +129 -0
- package/Models/DatabaseModels/GlobalConfig.ts +312 -0
- package/Models/DatabaseModels/Incident.ts +29 -0
- package/Models/DatabaseModels/IncidentCustomField.ts +1 -1
- package/Models/DatabaseModels/IncidentFeed.ts +1 -0
- package/Models/DatabaseModels/Index.ts +21 -8
- package/Models/DatabaseModels/InstanceHealthLog.ts +8 -0
- package/Models/DatabaseModels/MonitorCustomField.ts +1 -1
- package/Models/DatabaseModels/MonitorStatus.ts +1 -1
- package/Models/DatabaseModels/NetworkDeviceLabelRule.ts +2 -2
- package/Models/DatabaseModels/NetworkDeviceOwnerRule.ts +2 -2
- package/Models/DatabaseModels/NetworkSiteAssignmentRule.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +1 -1
- package/Models/DatabaseModels/Project.ts +231 -0
- package/Models/DatabaseModels/RumApplication.ts +906 -0
- package/Models/DatabaseModels/RumSessionErasureRequest.ts +561 -0
- package/Models/DatabaseModels/RumSessionPin.ts +457 -0
- package/Models/DatabaseModels/RumSessionReplayView.ts +406 -0
- package/Models/DatabaseModels/RunbookCredential.ts +673 -0
- package/Models/DatabaseModels/RunbookSecret.ts +7 -7
- package/Models/DatabaseModels/{RunbookAgent.ts → Runner.ts} +206 -46
- package/Models/DatabaseModels/{RunbookAgentJob.ts → RunnerJob.ts} +151 -18
- package/Models/DatabaseModels/{RunbookAgentOwnerTeam.ts → RunnerOwnerTeam.ts} +41 -43
- package/Models/DatabaseModels/{RunbookAgentOwnerUser.ts → RunnerOwnerUser.ts} +41 -43
- package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +1 -1
- package/Models/DatabaseModels/StatusPageCustomField.ts +1 -1
- package/Models/DatabaseModels/TeamCustomField.ts +1 -1
- package/Models/DatabaseModels/TeamMemberCustomField.ts +1 -1
- package/Server/API/AIAgentDataAPI.ts +179 -48
- package/Server/API/AIAgentTaskAPI.ts +36 -37
- package/Server/API/AIAgentTaskLogAPI.ts +19 -33
- package/Server/API/AutoRemediationAPI.ts +557 -0
- package/Server/API/BaseAnalyticsAPI.ts +4 -1
- package/Server/API/CommonAPI.ts +35 -0
- package/Server/API/DashboardAPI.ts +340 -15
- package/Server/API/TelemetryAPI.ts +1622 -3
- package/Server/API/UserIncomingCallNumberAPI.ts +31 -0
- package/Server/DatabaseConfig.ts +26 -1
- package/Server/EnvironmentConfig.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785417351021-AddSessionReplay.ts +293 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785491583874-EnableSessionReplayByDefault.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785496223183-AddMonitorSummaryToIncidentAndAlert.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785516996695-AddSessionReplayReliabilityColumns.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785533806494-AddSessionReplayCorrelationColumns.ts +54 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785749674203-AddRedisAndPostgresHealthNotificationColumns.ts +104 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785763818197-AddAutoRemediation.ts +349 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785768089408-AddRemediationVerification.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785776960660-AddRunnerCapabilities.ts +23 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785790000000-AddIncidentInvestigationGating.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785800000000-AddRunbookCredential.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785810000000-RenameRunbookAgentToRunner.ts +152 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785860000000-SessionReplayDefaultToSensitiveInputMasking.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785870000000-AddAiCommandRemediation.ts +151 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785880000000-AlignRunnerSchemaWithEntities.ts +597 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785890000000-AddFixVerificationAndAutomaticCodeFixes.ts +71 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +32 -0
- package/Server/Services/AIRunService.ts +10 -0
- package/Server/Services/AIService.ts +30 -0
- package/Server/Services/AlertService.ts +41 -4
- package/Server/Services/AlertSeverityService.ts +25 -11
- package/Server/Services/AlertStateService.ts +25 -11
- package/Server/Services/AutoRemediationRuleEngineService.ts +1101 -0
- package/Server/Services/{RunbookAgentOwnerTeamService.ts → AutoRemediationRuleService.ts} +1 -1
- package/Server/Services/AutoRemediationSuggestionService.ts +183 -0
- package/Server/Services/BillingService.ts +50 -1
- package/Server/Services/DatabaseService.ts +92 -4
- package/Server/Services/IncidentService.ts +56 -4
- package/Server/Services/IncidentSeverityService.ts +25 -11
- package/Server/Services/IncidentStateService.ts +25 -11
- package/Server/Services/Index.ts +19 -0
- package/Server/Services/MonitorService.ts +24 -0
- package/Server/Services/MonitorStatusService.ts +25 -11
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +18 -19
- package/Server/Services/NetworkDeviceLabelRuleService.ts +39 -0
- package/Server/Services/NetworkDeviceOwnerRuleEngineService.ts +18 -19
- package/Server/Services/NetworkDeviceOwnerRuleService.ts +37 -0
- package/Server/Services/NetworkDeviceService.ts +172 -21
- package/Server/Services/NetworkSiteAssignmentRuleService.ts +92 -12
- package/Server/Services/NetworkSiteService.ts +27 -10
- package/Server/Services/ProjectService.ts +47 -0
- package/Server/Services/RumApplicationService.ts +240 -1
- package/Server/Services/RumSessionChunkService.ts +11 -0
- package/Server/Services/RumSessionErasureRequestService.ts +197 -0
- package/Server/Services/RumSessionPinService.ts +213 -0
- package/Server/Services/RumSessionReplayViewService.ts +201 -0
- package/Server/Services/RumSessionService.ts +123 -0
- package/Server/Services/RunbookCredentialService.ts +76 -0
- package/Server/Services/{RunbookAgentJobService.ts → RunnerJobService.ts} +202 -51
- package/Server/Services/{RunbookAgentOwnerUserService.ts → RunnerOwnerTeamService.ts} +1 -1
- package/Server/Services/RunnerOwnerUserService.ts +10 -0
- package/Server/Services/RunnerService.ts +172 -0
- package/Server/Services/ScheduledMaintenanceStateService.ts +25 -11
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +15 -1
- package/Server/Services/ServiceService.ts +8 -2
- package/Server/Services/StatusPageService.ts +240 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +20 -1
- package/Server/Services/TelemetryExceptionService.ts +3 -16
- package/Server/Services/TelemetryUsageBillingService.ts +33 -2
- package/Server/Services/TraceAggregationService.ts +196 -32
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +17 -0
- package/Server/Types/Billing/MeteredPlan/ServerMeteredPlan.ts +10 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +63 -7
- package/Server/Utils/AI/CodeFix/CodeFixAgentAuth.ts +128 -0
- package/Server/Utils/AI/CodeFix/CodeFixAgentAvailability.ts +54 -0
- package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +17 -10
- package/Server/Utils/AI/CodeFix/CodeFixRunQueue.ts +8 -6
- package/Server/Utils/AI/Remediation/RemediationCommandTools.ts +943 -0
- package/Server/Utils/AI/Remediation/RemediationExecutionRunner.ts +866 -0
- package/Server/Utils/AI/Remediation/RemediationPlanRunner.ts +823 -0
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +103 -12
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +75 -17
- package/Server/Utils/AI/SRE/ConfidenceSignal.ts +28 -6
- package/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.ts +202 -12
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +264 -14
- package/Server/Utils/AI/SRE/Insights/Triage.ts +2 -7
- package/Server/Utils/AI/SRE/InvestigationGrader.ts +5 -24
- package/Server/Utils/AI/SRE/InvestigationQueue.ts +259 -20
- package/Server/Utils/AI/SRE/PostedRootCause.ts +82 -0
- package/Server/Utils/AI/SRE/README.md +3 -2
- package/Server/Utils/AI/SRE/RemediationHandoff.ts +82 -0
- package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +6 -19
- package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +2 -2
- package/Server/Utils/Alert/AlertPrivacyFilter.ts +58 -0
- package/Server/Utils/AutoRemediation/CommandPlanExecutor.ts +604 -0
- package/Server/Utils/AutoRemediation/RemediationVerifier.ts +619 -0
- package/Server/Utils/CorsOptions.ts +52 -0
- package/Server/Utils/Database/RelationIdUtil.ts +68 -0
- package/Server/Utils/Incident/IncidentPrivacyFilter.ts +60 -0
- package/Server/Utils/InstanceHealth/PostgresHealth.ts +286 -0
- package/Server/Utils/InstanceHealth/RedisHealth.ts +434 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +20 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +20 -0
- package/Server/Utils/Monitor/MonitorResource.ts +19 -0
- package/Server/Utils/Monitor/MonitorSummaryCapture.ts +104 -0
- package/Server/Utils/NetworkDevice/RulePatternValidator.ts +44 -0
- package/Server/Utils/OpenAPI.ts +2 -1
- package/Server/Utils/Runbook/RunbookExecutePermission.ts +94 -0
- package/Server/Utils/SessionReplay/SessionReplayErasureTombstone.ts +128 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +586 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +188 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1449 -0
- package/Server/Utils/SessionReplay/SessionReplayRetention.ts +114 -0
- package/Server/Utils/SessionReplay/SessionReplayTargeting.ts +144 -0
- package/Server/Utils/SessionReplay/SessionReplayUsage.ts +92 -0
- package/Server/Utils/StartServer.ts +47 -3
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +44 -10
- package/Tests/App/Dashboard/AddWidgetModal.test.tsx +557 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +479 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +343 -0
- package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +320 -0
- package/Tests/Models/AnalyticsModels/RumSessionSchema.test.ts +455 -0
- package/Tests/Models/AnalyticsModels/SessionIdCorrelation.test.ts +158 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +447 -0
- package/Tests/Models/RunnerVersionLabel.test.ts +223 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +1287 -0
- package/Tests/Server/API/LegacyRunbookAgentRoutes.test.ts +117 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +2093 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +429 -0
- package/Tests/Server/API/UserSmsApi.test.ts +68 -2
- package/Tests/Server/DatabaseConfig.test.ts +150 -0
- package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +2 -2
- package/Tests/Server/Infrastructure/ProcessSignalDelivery.test.ts +2 -3
- package/Tests/Server/Services/AIRunCodeFixClaimScoping.test.ts +100 -0
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +8 -0
- package/Tests/Server/Services/AutoRemediationCommandRuleEngine.test.ts +334 -0
- package/Tests/Server/Services/AutoRemediationRuleEngineService.test.ts +517 -0
- package/Tests/Server/Services/BillingService.test.ts +158 -1
- package/Tests/Server/Services/DatabaseServiceEmptyUpdate.test.ts +24 -2
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +17 -10
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsWithoutHooks.test.ts +128 -0
- package/Tests/Server/Services/DatabaseServiceUpdateWriteRouting.test.ts +178 -0
- package/Tests/Server/Services/DatabaseServiceUpdateWriteRoutingMonitor.test.ts +206 -0
- package/Tests/Server/Services/MonitorServiceOperationalStatusSelection.test.ts +145 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +726 -0
- package/Tests/Server/Services/NetworkDeviceSiteAssignment.test.ts +570 -9
- package/Tests/Server/Services/NetworkSiteAssignmentRuleService.test.ts +94 -1
- package/Tests/Server/Services/NetworkSiteService.test.ts +85 -0
- package/Tests/Server/Services/RumApplicationReplayHealth.test.ts +114 -0
- package/Tests/Server/Services/RunbookCredentialService.test.ts +657 -0
- package/Tests/Server/Services/SessionReplayCreatePath.test.ts +376 -0
- package/Tests/Server/Services/SessionReplayGateCacheInvalidation.test.ts +211 -0
- package/Tests/Server/Services/SessionReplayMetering.test.ts +174 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +555 -0
- package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +25 -4
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +17 -2
- package/Tests/Server/Services/TraceAggregationService.test.ts +48 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +855 -0
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +38 -27
- package/Tests/Server/Utils/AI/AIConfidenceSignal.test.ts +44 -0
- package/Tests/Server/Utils/AI/AIIncidentGating.test.ts +475 -0
- package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAuth.test.ts +257 -0
- package/Tests/Server/Utils/AI/CodeFix/CodeFixAgentAvailability.test.ts +236 -0
- package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +44 -6
- package/Tests/Server/Utils/AI/CodeFixRunQueue.test.ts +47 -1
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +2 -2
- package/Tests/Server/Utils/AI/FixFromIncidentAutoTrigger.test.ts +294 -0
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/InvestigationQueueInsightSubject.test.ts +47 -3
- package/Tests/Server/Utils/AI/InvestigationQueueRemediationExecution.test.ts +233 -0
- package/Tests/Server/Utils/AI/InvestigationSettlement.test.ts +463 -0
- package/Tests/Server/Utils/AI/PostedRootCause.test.ts +254 -0
- package/Tests/Server/Utils/AI/RemediationAllowlistNormalization.test.ts +248 -0
- package/Tests/Server/Utils/AI/RemediationCommandTools.test.ts +1008 -0
- package/Tests/Server/Utils/AI/RemediationExecutionRunner.test.ts +887 -0
- package/Tests/Server/Utils/AI/RemediationHandoff.test.ts +306 -0
- package/Tests/Server/Utils/AI/RemediationPlanRunner.test.ts +745 -0
- package/Tests/Server/Utils/AI/SubjectCodeFixRunDedupe.test.ts +291 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanExecutor.test.ts +1050 -0
- package/Tests/Server/Utils/AutoRemediation/CommandPlanVerification.test.ts +350 -0
- package/Tests/Server/Utils/AutoRemediation/RemediationVerifier.test.ts +341 -0
- package/Tests/Server/Utils/CorsOptions.test.ts +119 -0
- package/Tests/Server/Utils/Database/RelationIdUtil.test.ts +120 -0
- package/Tests/Server/Utils/Database/ResolveReferenceId.test.ts +60 -0
- package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +337 -0
- package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +671 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryCapture.test.ts +205 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +313 -0
- package/Tests/Server/Utils/PushNotificationUtil.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +177 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +108 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayRetention.test.ts +229 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayTargeting.test.ts +246 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +110 -0
- package/Tests/Types/AI/AIRunStatus.test.ts +25 -0
- package/Tests/Types/AutoRemediation/AiRemediationCommandPlan.test.ts +1063 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +56 -0
- package/Tests/Types/CustomField/CustomFieldDropdownOption.test.ts +227 -0
- package/Tests/Types/Dashboard/DashboardTemplateInvariants.test.ts +811 -0
- package/Tests/Types/Dashboard/RumDashboardTemplate.test.ts +1238 -0
- package/Tests/Types/Day/DayOfWeek.test.ts +46 -0
- package/Tests/Types/Docker/DockerInventoryExtractor.test.ts +249 -0
- package/Tests/Types/Kubernetes/KubernetesInventoryExtractor.test.ts +231 -0
- package/Tests/Types/Kubernetes/KubernetesRightSizing.test.ts +323 -0
- package/Tests/Types/Metrics/MetricDownsamplingRetentionDays.test.ts +77 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +42 -14
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +2 -2
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +3 -2
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +6 -3
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +285 -0
- package/Tests/Types/Podman/PodmanInventoryExtractor.test.ts +159 -0
- package/Tests/Types/Runbook/RunbookExecutionDeadline.test.ts +268 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +242 -0
- package/Tests/Types/Telemetry/TelemetryRetentionConfig.test.ts +225 -0
- package/Tests/Types/Text.test.ts +60 -0
- package/Tests/UI/Components/AutocompleteTextInputMenu.test.tsx +555 -0
- package/Tests/UI/Components/BulkLabelActions.test.tsx +1502 -0
- package/Tests/UI/Components/BulkOwnerActions.test.tsx +1255 -0
- package/Tests/UI/Components/CustomFields/DropdownOptionsInput.test.tsx +228 -0
- package/Tests/UI/Components/Detail/DropdownColors.test.tsx +128 -0
- package/Tests/UI/Components/Dropdown/DropdownValueBadge.test.tsx +60 -0
- package/Tests/UI/Components/FilterModalOperatorMenu.test.tsx +674 -0
- package/Tests/UI/Components/Forms/ColorPicker.test.tsx +48 -0
- package/Tests/UI/Components/Forms/ColorPickerPopup.test.tsx +222 -0
- package/Tests/UI/Components/Forms/IconPickerPopup.test.tsx +270 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +125 -6
- package/Tests/UI/Components/OperatorSelectorFilterTypes.test.tsx +1595 -0
- package/Tests/UI/Components/OperatorSelectorKeyboard.test.tsx +851 -0
- package/Tests/UI/Components/OperatorSelectorMenu.test.tsx +1229 -0
- package/Tests/UI/Components/Pagination.test.tsx +85 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +248 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +790 -0
- package/Tests/UI/Rum/ReplayScrubber.test.tsx +236 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +606 -0
- package/Tests/UI/Utils/Timezone.test.ts +86 -0
- package/Tests/UI/Utils/UseClockTick.test.tsx +276 -0
- package/Tests/Utils/AiRemediation/CommandPolicy.test.ts +1063 -0
- package/Tests/Utils/Dashboard/ClockWidgetFormat.test.ts +1374 -0
- package/Tests/Utils/Dashboard/DashboardClockComponent.test.ts +362 -0
- package/Tests/Utils/Dashboard/DashboardHtmlComponent.test.ts +217 -0
- package/Tests/Utils/Dashboard/HtmlWidgetDocument.test.ts +656 -0
- package/Tests/Utils/Dashboard/ValueWidgetLayout.test.ts +1007 -0
- package/Tests/Utils/Metrics/MetricSeriesScope.test.ts +1197 -0
- package/Tests/Utils/Monitor/MonitorSummarySnapshotUtil.test.ts +839 -0
- package/Tests/Utils/NetworkSite/CidrMatchUtil.test.ts +69 -0
- package/Tests/Utils/Rules/RulePatternMatchUtil.test.ts +274 -0
- package/Tests/Utils/Rum/ChunkMath.test.ts +286 -0
- package/Tests/Utils/Rum/Masking.test.ts +186 -0
- package/Tests/Utils/Rum/SessionIdentity.test.ts +272 -0
- package/Tests/Utils/Rum/SessionSampling.test.ts +164 -0
- package/Tests/Utils/Rum/UrlScrubber.test.ts +175 -0
- package/Tests/Utils/StatusPage/Report.test.ts +708 -0
- package/Tests/Utils/ValueFormatter.test.ts +299 -1
- package/Types/AI/AIRunStatus.ts +18 -7
- package/Types/AI/AIRunType.ts +18 -0
- package/Types/AI/FixVerificationStatus.ts +21 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +16 -0
- package/Types/AutoRemediation/AiRemediationCommandPlan.ts +304 -0
- package/Types/AutoRemediation/AutoRemediationExecutionMode.ts +17 -0
- package/Types/AutoRemediation/AutoRemediationSuggestionStatus.ts +35 -0
- package/Types/AutoRemediation/AutoRemediationSuggestionType.ts +17 -0
- package/Types/AutoRemediation/AutoRemediationTriggerEntity.ts +11 -0
- package/Types/AutoRemediation/AutoRemediationVerificationStatus.ts +21 -0
- package/Types/CustomField/CustomFieldDropdownOption.ts +190 -0
- package/Types/Dashboard/DashboardComponentType.ts +2 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +4 -0
- package/Types/Dashboard/DashboardComponents/DashboardClockComponent.ts +45 -0
- package/Types/Dashboard/DashboardComponents/DashboardHtmlComponent.ts +27 -0
- package/Types/Dashboard/DashboardComponents/DashboardValueComponent.ts +8 -0
- package/Types/Dashboard/DashboardTemplates.ts +516 -0
- package/Types/Date.ts +6 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Kubernetes/KubernetesRightSizing.ts +353 -0
- package/Types/MeteredPlan/ProductType.ts +1 -0
- package/Types/Monitor/MonitorStepMetricMonitor.ts +27 -1
- package/Types/Monitor/MonitorSteps.ts +5 -0
- package/Types/Monitor/MonitorSummarySnapshot.ts +88 -0
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +52 -10
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +7 -5
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +101 -20
- package/Types/Monitor/RumAlertTemplates.ts +386 -0
- package/Types/Permission.ts +204 -24
- package/Types/Rum/SessionReplay.ts +452 -0
- package/Types/Rum/SessionReplayCaptureTrigger.ts +26 -0
- package/Types/Rum/SessionReplayConsentMode.ts +27 -0
- package/Types/Rum/SessionReplayMaskingMode.ts +86 -0
- package/Types/Rum/SessionReplayTriggerReason.ts +31 -0
- package/Types/Runbook/RunbookCredentialType.ts +6 -0
- package/Types/Runbook/RunbookStep.ts +66 -1
- package/Types/Runbook/RunbookStepType.ts +26 -0
- package/Types/Runbook/RunnerJobOrigin.ts +20 -0
- package/Types/Runbook/{RunbookAgentJobStatus.ts → RunnerJobStatus.ts} +2 -2
- package/Types/StatusPage/StatusPageReport.ts +99 -0
- package/Types/Telemetry/TelemetryRetentionConfig.ts +18 -1
- package/Types/Text.ts +33 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +260 -75
- package/UI/Components/BulkUpdate/BulkLabelActions.tsx +99 -9
- package/UI/Components/BulkUpdate/BulkOwnerActions.tsx +11 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +19 -14
- package/UI/Components/CustomFields/DropdownOptionsInput.tsx +144 -54
- package/UI/Components/Detail/Detail.tsx +9 -22
- package/UI/Components/Dropdown/Dropdown.tsx +2 -5
- package/UI/Components/Dropdown/DropdownMenuZIndex.ts +7 -0
- package/UI/Components/Dropdown/DropdownValueBadge.tsx +61 -0
- package/UI/Components/Filters/OperatorSelector.tsx +350 -39
- package/UI/Components/Forms/Fields/ColorPicker.tsx +60 -27
- package/UI/Components/Forms/Fields/IconPicker.tsx +89 -60
- package/UI/Components/Forms/Validation.ts +9 -4
- package/UI/Components/List/List.tsx +1 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +41 -32
- package/UI/Components/Pagination/Pagination.tsx +33 -10
- package/UI/Components/Table/Table.tsx +1 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +321 -0
- package/UI/Utils/Dropdown.ts +26 -0
- package/UI/Utils/Timezone.ts +18 -2
- package/UI/Utils/UseClockTick.ts +78 -0
- package/Utils/AiRemediation/CommandPolicy.ts +398 -0
- package/Utils/Dashboard/ClockWidgetFormat.ts +604 -0
- package/Utils/Dashboard/Components/DashboardClockComponent.ts +181 -0
- package/Utils/Dashboard/Components/DashboardHtmlComponent.ts +162 -0
- package/Utils/Dashboard/Components/DashboardValueComponent.ts +12 -1
- package/Utils/Dashboard/Components/Index.ts +14 -0
- package/Utils/Dashboard/HtmlWidgetDocument.ts +309 -0
- package/Utils/Dashboard/ValueWidgetLayout.ts +471 -0
- package/Utils/Metrics/MetricSeriesScope.ts +301 -0
- package/Utils/Monitor/MonitorSummarySnapshotUtil.ts +593 -0
- package/Utils/NetworkSite/CidrMatchUtil.ts +30 -17
- package/Utils/Rules/RulePatternMatchUtil.ts +103 -0
- package/Utils/Rum/ChunkMath.ts +211 -0
- package/Utils/Rum/Masking.ts +168 -0
- package/Utils/Rum/SessionIdentity.ts +230 -0
- package/Utils/Rum/SessionSampling.ts +108 -0
- package/Utils/Rum/UrlScrubber.ts +185 -0
- package/Utils/Schema/ModelSchema.ts +27 -3
- package/Utils/StatusPage/Report.ts +304 -0
- package/Utils/ValueFormatter.ts +300 -54
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +63 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Index.js +10 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +56 -0
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/RumSession.js +1159 -0
- package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +670 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Span.js +56 -0
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +54 -0
- package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +32 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Alert.js +30 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertFeed.js +1 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AutoRemediationRule.js +998 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js +917 -0
- package/build/dist/Models/DatabaseModels/AutoRemediationSuggestion.js.map +1 -0
- package/build/dist/Models/DatabaseModels/CodeRepository.js +132 -0
- package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +327 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +30 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +20 -8
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InstanceHealthLog.js +8 -0
- package/build/dist/Models/DatabaseModels/InstanceHealthLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatus.js +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatus.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLabelRule.js +2 -2
- package/build/dist/Models/DatabaseModels/NetworkDeviceLabelRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerRule.js +2 -2
- package/build/dist/Models/DatabaseModels/NetworkDeviceOwnerRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +235 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +930 -0
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumSessionErasureRequest.js +591 -0
- package/build/dist/Models/DatabaseModels/RumSessionErasureRequest.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RumSessionPin.js +483 -0
- package/build/dist/Models/DatabaseModels/RumSessionPin.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RumSessionReplayView.js +433 -0
- package/build/dist/Models/DatabaseModels/RumSessionReplayView.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RunbookCredential.js +693 -0
- package/build/dist/Models/DatabaseModels/RunbookCredential.js.map +1 -0
- package/build/dist/Models/DatabaseModels/RunbookSecret.js +8 -8
- package/build/dist/Models/DatabaseModels/RunbookSecret.js.map +1 -1
- package/build/dist/Models/DatabaseModels/{RunbookAgent.js → Runner.js} +235 -70
- package/build/dist/Models/DatabaseModels/Runner.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{RunbookAgentJob.js → RunnerJob.js} +173 -40
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{RunbookAgentOwnerTeam.js → RunnerOwnerTeam.js} +57 -57
- package/build/dist/Models/DatabaseModels/RunnerOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{RunbookAgentOwnerUser.js → RunnerOwnerUser.js} +57 -57
- package/build/dist/Models/DatabaseModels/RunnerOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +1 -1
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +130 -45
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskAPI.js +26 -36
- package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
- package/build/dist/Server/API/AIAgentTaskLogAPI.js +11 -28
- package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
- package/build/dist/Server/API/AutoRemediationAPI.js +416 -0
- package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -0
- package/build/dist/Server/API/BaseAnalyticsAPI.js +4 -1
- package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +24 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +265 -6
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +940 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +18 -0
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/DatabaseConfig.js +23 -4
- package/build/dist/Server/DatabaseConfig.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +8 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785417351021-AddSessionReplay.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785417351021-AddSessionReplay.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785491583874-EnableSessionReplayByDefault.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785491583874-EnableSessionReplayByDefault.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785496223183-AddMonitorSummaryToIncidentAndAlert.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785496223183-AddMonitorSummaryToIncidentAndAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785516996695-AddSessionReplayReliabilityColumns.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785516996695-AddSessionReplayReliabilityColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785533806494-AddSessionReplayCorrelationColumns.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785533806494-AddSessionReplayCorrelationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785749674203-AddRedisAndPostgresHealthNotificationColumns.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785749674203-AddRedisAndPostgresHealthNotificationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785763818197-AddAutoRemediation.js +128 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785763818197-AddAutoRemediation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785768089408-AddRemediationVerification.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785768089408-AddRemediationVerification.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785776960660-AddRunnerCapabilities.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785776960660-AddRunnerCapabilities.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785790000000-AddIncidentInvestigationGating.js +27 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785790000000-AddIncidentInvestigationGating.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785800000000-AddRunbookCredential.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785800000000-AddRunbookCredential.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785810000000-RenameRunbookAgentToRunner.js +88 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785810000000-RenameRunbookAgentToRunner.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785860000000-SessionReplayDefaultToSensitiveInputMasking.js +59 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785860000000-SessionReplayDefaultToSensitiveInputMasking.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785870000000-AddAiCommandRemediation.js +76 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785870000000-AddAiCommandRemediation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785880000000-AlignRunnerSchemaWithEntities.js +214 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785880000000-AlignRunnerSchemaWithEntities.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785890000000-AddFixVerificationAndAutomaticCodeFixes.js +44 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785890000000-AddFixVerificationAndAutomaticCodeFixes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +1 -4
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +26 -0
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +37 -4
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +24 -11
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/AlertStateService.js +24 -11
- package/build/dist/Server/Services/AlertStateService.js.map +1 -1
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js +917 -0
- package/build/dist/Server/Services/AutoRemediationRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/{RunbookAgentOwnerTeamService.js → AutoRemediationRuleService.js} +2 -2
- package/build/dist/Server/Services/AutoRemediationRuleService.js.map +1 -0
- package/build/dist/Server/Services/AutoRemediationSuggestionService.js +127 -0
- package/build/dist/Server/Services/AutoRemediationSuggestionService.js.map +1 -0
- package/build/dist/Server/Services/BillingService.js +45 -1
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +72 -4
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +52 -4
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +24 -11
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateService.js +24 -11
- package/build/dist/Server/Services/IncidentStateService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +18 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +24 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusService.js +25 -11
- package/build/dist/Server/Services/MonitorStatusService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +12 -11
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleService.js +43 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js +12 -11
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleService.js +41 -0
- package/build/dist/Server/Services/NetworkDeviceOwnerRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +129 -20
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js +67 -10
- package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +19 -8
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +43 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +202 -0
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionChunkService.js +9 -0
- package/build/dist/Server/Services/RumSessionChunkService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionErasureRequestService.js +190 -0
- package/build/dist/Server/Services/RumSessionErasureRequestService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionPinService.js +208 -0
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionReplayViewService.js +180 -0
- package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionService.js +102 -0
- package/build/dist/Server/Services/RumSessionService.js.map +1 -0
- package/build/dist/Server/Services/RunbookCredentialService.js +67 -0
- package/build/dist/Server/Services/RunbookCredentialService.js.map +1 -0
- package/build/dist/Server/Services/{RunbookAgentJobService.js → RunnerJobService.js} +158 -46
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -0
- package/build/dist/Server/Services/{RunbookAgentOwnerUserService.js → RunnerOwnerTeamService.js} +2 -2
- package/build/dist/Server/Services/RunnerOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/RunnerOwnerUserService.js +9 -0
- package/build/dist/Server/Services/RunnerOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/RunnerService.js +168 -0
- package/build/dist/Server/Services/RunnerService.js.map +1 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateService.js +24 -11
- package/build/dist/Server/Services/ScheduledMaintenanceStateService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +15 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +8 -2
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +173 -16
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryExceptionService.js +3 -16
- package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +31 -3
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +106 -2
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +16 -0
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/ServerMeteredPlan.js +9 -0
- package/build/dist/Server/Types/Billing/MeteredPlan/ServerMeteredPlan.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +38 -6
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js +104 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAuth.js.map +1 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAvailability.js +54 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentAvailability.js.map +1 -0
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +11 -7
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixRunQueue.js +3 -4
- package/build/dist/Server/Utils/AI/CodeFix/CodeFixRunQueue.js.map +1 -1
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js +724 -0
- package/build/dist/Server/Utils/AI/Remediation/RemediationCommandTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js +668 -0
- package/build/dist/Server/Utils/AI/Remediation/RemediationExecutionRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js +657 -0
- package/build/dist/Server/Utils/AI/Remediation/RemediationPlanRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +52 -7
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +53 -5
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/ConfidenceSignal.js +25 -6
- package/build/dist/Server/Utils/AI/SRE/ConfidenceSignal.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.js +131 -17
- package/build/dist/Server/Utils/AI/SRE/FixFromIncidentTaskTrigger.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +200 -5
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Triage.js +2 -7
- package/build/dist/Server/Utils/AI/SRE/Insights/Triage.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js +4 -17
- package/build/dist/Server/Utils/AI/SRE/InvestigationGrader.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js +203 -19
- package/build/dist/Server/Utils/AI/SRE/InvestigationQueue.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/PostedRootCause.js +89 -0
- package/build/dist/Server/Utils/AI/SRE/PostedRootCause.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/RemediationHandoff.js +82 -0
- package/build/dist/Server/Utils/AI/SRE/RemediationHandoff.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +4 -19
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +2 -2
- package/build/dist/Server/Utils/Alert/AlertPrivacyFilter.js +36 -0
- package/build/dist/Server/Utils/Alert/AlertPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js +498 -0
- package/build/dist/Server/Utils/AutoRemediation/CommandPlanExecutor.js.map +1 -0
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js +492 -0
- package/build/dist/Server/Utils/AutoRemediation/RemediationVerifier.js.map +1 -0
- package/build/dist/Server/Utils/CorsOptions.js +47 -0
- package/build/dist/Server/Utils/CorsOptions.js.map +1 -0
- package/build/dist/Server/Utils/Database/RelationIdUtil.js +51 -0
- package/build/dist/Server/Utils/Database/RelationIdUtil.js.map +1 -0
- package/build/dist/Server/Utils/Incident/IncidentPrivacyFilter.js +38 -0
- package/build/dist/Server/Utils/Incident/IncidentPrivacyFilter.js.map +1 -1
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +194 -0
- package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +1 -0
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +292 -0
- package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +9 -0
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +16 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorSummaryCapture.js +92 -0
- package/build/dist/Server/Utils/Monitor/MonitorSummaryCapture.js.map +1 -0
- package/build/dist/Server/Utils/NetworkDevice/RulePatternValidator.js +27 -0
- package/build/dist/Server/Utils/NetworkDevice/RulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/OpenAPI.js +2 -1
- package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js +67 -0
- package/build/dist/Server/Utils/Runbook/RunbookExecutePermission.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayErasureTombstone.js +98 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayErasureTombstone.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +378 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +137 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +998 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayRetention.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayRetention.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayTargeting.js +98 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayTargeting.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js +69 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayUsage.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +40 -3
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +27 -6
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
- package/build/dist/Types/AI/AIRunStatus.js +17 -5
- package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
- package/build/dist/Types/AI/AIRunType.js +18 -0
- package/build/dist/Types/AI/AIRunType.js.map +1 -1
- package/build/dist/Types/AI/FixVerificationStatus.js +22 -0
- package/build/dist/Types/AI/FixVerificationStatus.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +16 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js +192 -0
- package/build/dist/Types/AutoRemediation/AiRemediationCommandPlan.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationExecutionMode.js +18 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationExecutionMode.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationSuggestionStatus.js +31 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationSuggestionStatus.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationSuggestionType.js +18 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationSuggestionType.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationTriggerEntity.js +12 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationTriggerEntity.js.map +1 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationVerificationStatus.js +22 -0
- package/build/dist/Types/AutoRemediation/AutoRemediationVerificationStatus.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldDropdownOption.js +110 -0
- package/build/dist/Types/CustomField/CustomFieldDropdownOption.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +4 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardClockComponent.js +19 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardClockComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardHtmlComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardHtmlComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +487 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Date.js +6 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesRightSizing.js +229 -0
- package/build/dist/Types/Kubernetes/KubernetesRightSizing.js.map +1 -0
- package/build/dist/Types/MeteredPlan/ProductType.js +1 -0
- package/build/dist/Types/MeteredPlan/ProductType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js +12 -2
- package/build/dist/Types/Monitor/MonitorStepMetricMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorSteps.js +5 -0
- package/build/dist/Types/Monitor/MonitorSteps.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorSummarySnapshot.js +13 -0
- package/build/dist/Types/Monitor/MonitorSummarySnapshot.js.map +1 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +40 -8
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +4 -3
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +50 -10
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/RumAlertTemplates.js +297 -0
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +184 -24
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +155 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +26 -0
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayConsentMode.js +27 -0
- package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayMaskingMode.js +79 -0
- package/build/dist/Types/Rum/SessionReplayMaskingMode.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayTriggerReason.js +28 -0
- package/build/dist/Types/Rum/SessionReplayTriggerReason.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookCredentialType.js +7 -0
- package/build/dist/Types/Runbook/RunbookCredentialType.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStep.js +20 -1
- package/build/dist/Types/Runbook/RunbookStep.js.map +1 -1
- package/build/dist/Types/Runbook/RunbookStepType.js +24 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/Types/Runbook/RunnerJobOrigin.js +21 -0
- package/build/dist/Types/Runbook/RunnerJobOrigin.js.map +1 -0
- package/build/dist/Types/Runbook/RunnerJobStatus.js +22 -0
- package/build/dist/Types/Runbook/RunnerJobStatus.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageReport.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReport.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryRetentionConfig.js.map +1 -1
- package/build/dist/Types/Text.js +29 -0
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +143 -28
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js +82 -9
- package/build/dist/UI/Components/BulkUpdate/BulkLabelActions.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js +11 -0
- package/build/dist/UI/Components/BulkUpdate/BulkOwnerActions.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +11 -13
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js +76 -45
- package/build/dist/UI/Components/CustomFields/DropdownOptionsInput.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +3 -8
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js +2 -5
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Dropdown/DropdownMenuZIndex.js +7 -0
- package/build/dist/UI/Components/Dropdown/DropdownMenuZIndex.js.map +1 -0
- package/build/dist/UI/Components/Dropdown/DropdownValueBadge.js +32 -0
- package/build/dist/UI/Components/Dropdown/DropdownValueBadge.js.map +1 -0
- package/build/dist/UI/Components/Filters/OperatorSelector.js +210 -20
- package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +26 -10
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -13
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +9 -5
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +1 -1
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +21 -19
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +14 -3
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +1 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +197 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -0
- package/build/dist/UI/Utils/Dropdown.js +18 -0
- package/build/dist/UI/Utils/Dropdown.js.map +1 -1
- package/build/dist/UI/Utils/Timezone.js +13 -2
- package/build/dist/UI/Utils/Timezone.js.map +1 -1
- package/build/dist/UI/Utils/UseClockTick.js +41 -0
- package/build/dist/UI/Utils/UseClockTick.js.map +1 -0
- package/build/dist/Utils/AiRemediation/CommandPolicy.js +303 -0
- package/build/dist/Utils/AiRemediation/CommandPolicy.js.map +1 -0
- package/build/dist/Utils/Dashboard/ClockWidgetFormat.js +232 -0
- package/build/dist/Utils/Dashboard/ClockWidgetFormat.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js +146 -0
- package/build/dist/Utils/Dashboard/Components/DashboardClockComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardHtmlComponent.js +133 -0
- package/build/dist/Utils/Dashboard/Components/DashboardHtmlComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js +10 -1
- package/build/dist/Utils/Dashboard/Components/DashboardValueComponent.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/Index.js +8 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js +198 -0
- package/build/dist/Utils/Dashboard/HtmlWidgetDocument.js.map +1 -0
- package/build/dist/Utils/Dashboard/ValueWidgetLayout.js +276 -0
- package/build/dist/Utils/Dashboard/ValueWidgetLayout.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricSeriesScope.js +203 -0
- package/build/dist/Utils/Metrics/MetricSeriesScope.js.map +1 -0
- package/build/dist/Utils/Monitor/MonitorSummarySnapshotUtil.js +382 -0
- package/build/dist/Utils/Monitor/MonitorSummarySnapshotUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/CidrMatchUtil.js +13 -7
- package/build/dist/Utils/NetworkSite/CidrMatchUtil.js.map +1 -1
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +87 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js.map +1 -0
- package/build/dist/Utils/Rum/ChunkMath.js +147 -0
- package/build/dist/Utils/Rum/ChunkMath.js.map +1 -0
- package/build/dist/Utils/Rum/Masking.js +143 -0
- package/build/dist/Utils/Rum/Masking.js.map +1 -0
- package/build/dist/Utils/Rum/SessionIdentity.js +161 -0
- package/build/dist/Utils/Rum/SessionIdentity.js.map +1 -0
- package/build/dist/Utils/Rum/SessionSampling.js +91 -0
- package/build/dist/Utils/Rum/SessionSampling.js.map +1 -0
- package/build/dist/Utils/Rum/UrlScrubber.js +144 -0
- package/build/dist/Utils/Rum/UrlScrubber.js.map +1 -0
- package/build/dist/Utils/Schema/ModelSchema.js +28 -3
- package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
- package/build/dist/Utils/StatusPage/Report.js +194 -0
- package/build/dist/Utils/StatusPage/Report.js.map +1 -0
- package/build/dist/Utils/ValueFormatter.js +239 -46
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/jest.config.json +3 -0
- package/package.json +1 -1
- package/tsconfig.json +19 -1
- package/Server/Services/RunbookAgentService.ts +0 -93
- package/build/dist/Models/DatabaseModels/RunbookAgent.js.map +0 -1
- package/build/dist/Models/DatabaseModels/RunbookAgentJob.js.map +0 -1
- package/build/dist/Models/DatabaseModels/RunbookAgentOwnerTeam.js.map +0 -1
- package/build/dist/Models/DatabaseModels/RunbookAgentOwnerUser.js.map +0 -1
- package/build/dist/Server/Services/RunbookAgentJobService.js.map +0 -1
- package/build/dist/Server/Services/RunbookAgentOwnerTeamService.js.map +0 -1
- package/build/dist/Server/Services/RunbookAgentOwnerUserService.js.map +0 -1
- package/build/dist/Server/Services/RunbookAgentService.js +0 -91
- package/build/dist/Server/Services/RunbookAgentService.js.map +0 -1
- package/build/dist/Types/Runbook/RunbookAgentJobStatus.js +0 -22
- package/build/dist/Types/Runbook/RunbookAgentJobStatus.js.map +0 -1
|
@@ -0,0 +1,2093 @@
|
|
|
1
|
+
import CommonAPI from "../../../Server/API/CommonAPI";
|
|
2
|
+
import UserMiddleware from "../../../Server/Middleware/UserAuthorization";
|
|
3
|
+
import ProjectService from "../../../Server/Services/ProjectService";
|
|
4
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
5
|
+
import RumApplicationService from "../../../Server/Services/RumApplicationService";
|
|
6
|
+
import RumSessionReplayViewService from "../../../Server/Services/RumSessionReplayViewService";
|
|
7
|
+
import RumSessionService from "../../../Server/Services/RumSessionService";
|
|
8
|
+
import RumSessionChunkService from "../../../Server/Services/RumSessionChunkService";
|
|
9
|
+
import { Statement } from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
10
|
+
import RumApplication from "../../../Models/DatabaseModels/RumApplication";
|
|
11
|
+
import RumSessionReplayView from "../../../Models/DatabaseModels/RumSessionReplayView";
|
|
12
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
13
|
+
import {
|
|
14
|
+
ExpressRequest,
|
|
15
|
+
ExpressResponse,
|
|
16
|
+
NextFunction,
|
|
17
|
+
} from "../../../Server/Utils/Express";
|
|
18
|
+
import Response from "../../../Server/Utils/Response";
|
|
19
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
20
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
21
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
22
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
23
|
+
import Exception from "../../../Types/Exception/Exception";
|
|
24
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
25
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
26
|
+
import Permission, {
|
|
27
|
+
UserPermission,
|
|
28
|
+
UserTenantAccessPermission,
|
|
29
|
+
} from "../../../Types/Permission";
|
|
30
|
+
import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
|
|
31
|
+
import UserType from "../../../Types/UserType";
|
|
32
|
+
import { MAX_SESSION_REPLAY_CHUNKS_PER_READ } from "../../../Types/Rum/SessionReplay";
|
|
33
|
+
import {
|
|
34
|
+
afterEach,
|
|
35
|
+
beforeAll,
|
|
36
|
+
beforeEach,
|
|
37
|
+
describe,
|
|
38
|
+
expect,
|
|
39
|
+
test,
|
|
40
|
+
} from "@jest/globals";
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* The session-replay playback routes are the ONLY reader of the two
|
|
44
|
+
* replay analytics tables: both models omit `crudApiPath`, so
|
|
45
|
+
* ModelPermission is never invoked and the `payload` column's own read
|
|
46
|
+
* ACL is, by itself, decorative. Every guarantee therefore has to be
|
|
47
|
+
* proved at the handler, which is what this file does.
|
|
48
|
+
*
|
|
49
|
+
* A note on status codes. The requirement these tests were written
|
|
50
|
+
* against says an under-privileged caller "gets 401". This repo maps
|
|
51
|
+
* NotAuthenticatedException to 401 (not logged in) and
|
|
52
|
+
* NotAuthorizedException to 422 (logged in, insufficient rights) - see
|
|
53
|
+
* Types/Exception/ExceptionCode.ts - and every permission guard in the
|
|
54
|
+
* codebase, including UserMiddleware.requirePermission, raises the
|
|
55
|
+
* latter. Emitting 401 for an authenticated-but-unauthorized caller on
|
|
56
|
+
* these five routes alone would be both HTTP-incorrect and inconsistent
|
|
57
|
+
* with the rest of the API, so the tests assert the repo's real denial
|
|
58
|
+
* (NotAuthorizedException / 422). The security property being pinned -
|
|
59
|
+
* that a Viewer or ProjectMember cannot reach a recording - is identical.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
type RouterFunction = (
|
|
63
|
+
req: ExpressRequest,
|
|
64
|
+
res: ExpressResponse,
|
|
65
|
+
next: NextFunction,
|
|
66
|
+
) => void | Promise<void>;
|
|
67
|
+
|
|
68
|
+
type RecordedRoute = {
|
|
69
|
+
method: string;
|
|
70
|
+
uri: string;
|
|
71
|
+
handlers: Array<RouterFunction>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const recordedRoutes: Array<RecordedRoute> = [];
|
|
75
|
+
|
|
76
|
+
type RecordRouteFunction = (
|
|
77
|
+
method: string,
|
|
78
|
+
) => (uri: string, ...handlers: Array<RouterFunction>) => void;
|
|
79
|
+
|
|
80
|
+
const recordRoute: RecordRouteFunction = (method: string) => {
|
|
81
|
+
return (uri: string, ...handlers: Array<RouterFunction>): void => {
|
|
82
|
+
recordedRoutes.push({
|
|
83
|
+
method: method.toUpperCase(),
|
|
84
|
+
uri: uri,
|
|
85
|
+
handlers: handlers,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const sessionReplayRouter: JSONObject = {
|
|
91
|
+
get: recordRoute("get"),
|
|
92
|
+
post: recordRoute("post"),
|
|
93
|
+
put: recordRoute("put"),
|
|
94
|
+
delete: recordRoute("delete"),
|
|
95
|
+
} as unknown as JSONObject;
|
|
96
|
+
|
|
97
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
98
|
+
return {
|
|
99
|
+
getRouter: () => {
|
|
100
|
+
return sessionReplayRouter;
|
|
101
|
+
},
|
|
102
|
+
getClientIp: () => {
|
|
103
|
+
return "203.0.113.7";
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
109
|
+
return {
|
|
110
|
+
sendEntityArrayResponse: jest.fn(),
|
|
111
|
+
sendJsonObjectResponse: jest.fn(),
|
|
112
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
113
|
+
sendEntityResponse: jest.fn(),
|
|
114
|
+
sendErrorResponse: jest.fn(),
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const LIST_ROUTE: string = "/telemetry/rum/session-replay/list";
|
|
119
|
+
const MANIFEST_ROUTE: string = "/telemetry/rum/session-replay/manifest";
|
|
120
|
+
const CHUNKS_ROUTE: string = "/telemetry/rum/session-replay/chunks";
|
|
121
|
+
const HEARTBEAT_ROUTE: string = "/telemetry/rum/session-replay/heartbeat";
|
|
122
|
+
const FOR_EXCEPTION_ROUTE: string =
|
|
123
|
+
"/telemetry/rum/session-replay/for-exception";
|
|
124
|
+
const INGEST_STATUS_ROUTE: string =
|
|
125
|
+
"/telemetry/rum/session-replay/ingest-status";
|
|
126
|
+
|
|
127
|
+
function findRoute(uri: string): RecordedRoute {
|
|
128
|
+
const route: RecordedRoute | undefined = recordedRoutes.find(
|
|
129
|
+
(candidate: RecordedRoute): boolean => {
|
|
130
|
+
return candidate.method === "POST" && candidate.uri === uri;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!route) {
|
|
135
|
+
throw new Error(`Route not registered: ${uri}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return route;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type CallResult = {
|
|
142
|
+
/* Exception handed to Response.sendErrorResponse by a guard or handler. */
|
|
143
|
+
deniedWith: Exception | undefined;
|
|
144
|
+
/* Exception thrown out of the handler into express' error path. */
|
|
145
|
+
thrownToNext: unknown;
|
|
146
|
+
/* True when the final route handler was actually entered. */
|
|
147
|
+
reachedHandler: boolean;
|
|
148
|
+
jsonBody: JSONObject | undefined;
|
|
149
|
+
sentBuffer: Buffer | undefined;
|
|
150
|
+
headers: Dictionary<string>;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* Runs a recorded route's middleware chain for real, starting at
|
|
155
|
+
* requireUserAuthentication.
|
|
156
|
+
*
|
|
157
|
+
* Index 0 is UserMiddleware.getUserMiddleware, whose only job is to load
|
|
158
|
+
* the session and populate userType / tenantId / tenant permissions on
|
|
159
|
+
* the request - exactly the fields these fixtures set directly. Its
|
|
160
|
+
* presence is asserted separately rather than executed, because running
|
|
161
|
+
* it would require a live session store and would prove nothing about
|
|
162
|
+
* authorization.
|
|
163
|
+
*/
|
|
164
|
+
async function callRoute(data: {
|
|
165
|
+
uri: string;
|
|
166
|
+
request: JSONObject;
|
|
167
|
+
body: JSONObject;
|
|
168
|
+
}): Promise<CallResult> {
|
|
169
|
+
const route: RecordedRoute = findRoute(data.uri);
|
|
170
|
+
|
|
171
|
+
const headers: Dictionary<string> = {};
|
|
172
|
+
let sentBuffer: Buffer | undefined = undefined;
|
|
173
|
+
|
|
174
|
+
const req: ExpressRequest = {
|
|
175
|
+
...data.request,
|
|
176
|
+
body: data.body,
|
|
177
|
+
params: {},
|
|
178
|
+
query: {},
|
|
179
|
+
headers: { "user-agent": "jest-agent" },
|
|
180
|
+
} as unknown as ExpressRequest;
|
|
181
|
+
|
|
182
|
+
const res: ExpressResponse = {
|
|
183
|
+
setHeader: (key: string, value: string): void => {
|
|
184
|
+
headers[key] = value;
|
|
185
|
+
},
|
|
186
|
+
send: (payload: Buffer): void => {
|
|
187
|
+
sentBuffer = payload;
|
|
188
|
+
},
|
|
189
|
+
status: jest.fn().mockReturnThis(),
|
|
190
|
+
json: jest.fn(),
|
|
191
|
+
} as unknown as ExpressResponse;
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* Held in a const container rather than in loop-scoped `let`s so the
|
|
195
|
+
* per-iteration `next` closures never capture a mutable outer binding.
|
|
196
|
+
*/
|
|
197
|
+
const outcome: { thrownToNext: unknown; reachedHandler: boolean } = {
|
|
198
|
+
thrownToNext: undefined,
|
|
199
|
+
reachedHandler: false,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
for (let index: number = 1; index < route.handlers.length; index++) {
|
|
203
|
+
const handler: RouterFunction | undefined = route.handlers[index];
|
|
204
|
+
|
|
205
|
+
if (!handler) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (index === route.handlers.length - 1) {
|
|
210
|
+
outcome.reachedHandler = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const step: { calledNext: boolean } = { calledNext: false };
|
|
214
|
+
|
|
215
|
+
const next: NextFunction = ((error?: unknown): void => {
|
|
216
|
+
step.calledNext = true;
|
|
217
|
+
|
|
218
|
+
if (error) {
|
|
219
|
+
outcome.thrownToNext = error;
|
|
220
|
+
}
|
|
221
|
+
}) as unknown as NextFunction;
|
|
222
|
+
|
|
223
|
+
await handler(req, res, next);
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* A guard that answered the request itself never calls next, which
|
|
227
|
+
* is precisely the denial being asserted.
|
|
228
|
+
*/
|
|
229
|
+
if (!step.calledNext) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const sendErrorResponse: jest.Mock =
|
|
235
|
+
Response.sendErrorResponse as unknown as jest.Mock;
|
|
236
|
+
const sendJsonObjectResponse: jest.Mock =
|
|
237
|
+
Response.sendJsonObjectResponse as unknown as jest.Mock;
|
|
238
|
+
|
|
239
|
+
const errorCall: Array<unknown> | undefined = sendErrorResponse.mock
|
|
240
|
+
.calls[0] as Array<unknown> | undefined;
|
|
241
|
+
|
|
242
|
+
const jsonCall: Array<unknown> | undefined = sendJsonObjectResponse.mock
|
|
243
|
+
.calls[0] as Array<unknown> | undefined;
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
deniedWith: errorCall ? (errorCall[2] as Exception) : undefined,
|
|
247
|
+
thrownToNext: outcome.thrownToNext,
|
|
248
|
+
reachedHandler: outcome.reachedHandler,
|
|
249
|
+
jsonBody: jsonCall ? (jsonCall[2] as JSONObject) : undefined,
|
|
250
|
+
sentBuffer: sentBuffer,
|
|
251
|
+
headers: headers,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildPrincipal(data: {
|
|
256
|
+
projectId: ObjectID;
|
|
257
|
+
userId: ObjectID;
|
|
258
|
+
permissions: Array<Permission>;
|
|
259
|
+
labelIds?: Array<ObjectID> | undefined;
|
|
260
|
+
/*
|
|
261
|
+
* TeamPermission.scope is a real, admin-settable column (All / Owned /
|
|
262
|
+
* Labels), and Owned is excluded from BOTH PermissionHelper filters -
|
|
263
|
+
* so a fixture that never sets it cannot see the fail-open it causes.
|
|
264
|
+
*/
|
|
265
|
+
scope?: PermissionScope | undefined;
|
|
266
|
+
}): {
|
|
267
|
+
request: JSONObject;
|
|
268
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
269
|
+
} {
|
|
270
|
+
/*
|
|
271
|
+
* isBlockPermission must be an explicit false, not omitted:
|
|
272
|
+
* DatabaseCommonInteractionPropsUtil.getUserPermissions filters tenant
|
|
273
|
+
* permissions with a strict `isBlockPermission === false`, so an
|
|
274
|
+
* undefined here would silently drop every permission the fixture
|
|
275
|
+
* grants - which is exactly what AccessTokenService writes for a real
|
|
276
|
+
* team permission.
|
|
277
|
+
*/
|
|
278
|
+
const userPermissions: Array<UserPermission> = data.permissions.map(
|
|
279
|
+
(permission: Permission): UserPermission => {
|
|
280
|
+
return {
|
|
281
|
+
_type: "UserPermission",
|
|
282
|
+
permission: permission,
|
|
283
|
+
labelIds: data.labelIds || [],
|
|
284
|
+
isBlockPermission: false,
|
|
285
|
+
...(data.scope !== undefined && { scope: data.scope }),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
291
|
+
_type: "UserTenantAccessPermission",
|
|
292
|
+
projectId: data.projectId,
|
|
293
|
+
permissions: userPermissions,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const permissionMap: Dictionary<UserTenantAccessPermission> = {};
|
|
297
|
+
permissionMap[data.projectId.toString()] = tenantPermission;
|
|
298
|
+
|
|
299
|
+
const databaseProps: DatabaseCommonInteractionProps = {
|
|
300
|
+
tenantId: data.projectId,
|
|
301
|
+
userId: data.userId,
|
|
302
|
+
userType: UserType.User,
|
|
303
|
+
userTenantAccessPermission: permissionMap,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
request: {
|
|
308
|
+
userType: UserType.User,
|
|
309
|
+
tenantId: data.projectId,
|
|
310
|
+
userTenantAccessPermission: permissionMap,
|
|
311
|
+
userAuthorization: { userId: data.userId },
|
|
312
|
+
} as unknown as JSONObject,
|
|
313
|
+
databaseProps: databaseProps,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
* Adds an UNSCOPED ReadRumSessionReplay row on top of whatever the
|
|
319
|
+
* principal already holds. The list guard does not accept
|
|
320
|
+
* ReadRumSessionReplayPayload, so a payload-scoped reviewer needs this to
|
|
321
|
+
* get into /list at all - which is precisely the shape that makes the
|
|
322
|
+
* identity column's label scope worth checking separately.
|
|
323
|
+
*/
|
|
324
|
+
function grantUnscopedListPermission(
|
|
325
|
+
databaseProps: DatabaseCommonInteractionProps,
|
|
326
|
+
projectId: ObjectID,
|
|
327
|
+
): void {
|
|
328
|
+
const tenantPermission: UserTenantAccessPermission | undefined =
|
|
329
|
+
databaseProps.userTenantAccessPermission?.[projectId.toString()];
|
|
330
|
+
|
|
331
|
+
if (!tenantPermission) {
|
|
332
|
+
throw new Error("Principal has no tenant permission to extend.");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
tenantPermission.permissions.push({
|
|
336
|
+
_type: "UserPermission",
|
|
337
|
+
permission: Permission.ReadRumSessionReplay,
|
|
338
|
+
labelIds: [],
|
|
339
|
+
isBlockPermission: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Minimal stand-in for the ClickHouse client's ResultSet. */
|
|
344
|
+
function fakeResultSet(rows: Array<JSONObject>): unknown {
|
|
345
|
+
return {
|
|
346
|
+
json: async (): Promise<JSONObject> => {
|
|
347
|
+
return { data: rows } as unknown as JSONObject;
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildHeaderRow(data: {
|
|
353
|
+
sessionId: string;
|
|
354
|
+
projectId: ObjectID;
|
|
355
|
+
rumApplicationId: ObjectID;
|
|
356
|
+
}): JSONObject {
|
|
357
|
+
return {
|
|
358
|
+
sessionId: data.sessionId,
|
|
359
|
+
headerProjectId: data.projectId.toString(),
|
|
360
|
+
applicationId: data.rumApplicationId.toString(),
|
|
361
|
+
aggStartTime: 1700000000000,
|
|
362
|
+
aggEndTime: 1700000060000,
|
|
363
|
+
aggDurationMs: 60000,
|
|
364
|
+
aggIsFinalized: true,
|
|
365
|
+
aggSealedReason: "final-chunk",
|
|
366
|
+
aggChunkCount: 4,
|
|
367
|
+
aggMaxChunkIndex: 3,
|
|
368
|
+
aggMissingChunkCount: 0,
|
|
369
|
+
aggEventCount: 400,
|
|
370
|
+
aggPayloadBytes: 4096,
|
|
371
|
+
aggHasError: true,
|
|
372
|
+
aggErrorCount: 1,
|
|
373
|
+
aggRageClickCount: 0,
|
|
374
|
+
aggDeadClickCount: 0,
|
|
375
|
+
aggErrorClickCount: 0,
|
|
376
|
+
aggRefreshRageCount: 0,
|
|
377
|
+
aggPageCount: 2,
|
|
378
|
+
aggTriggerReason: "error",
|
|
379
|
+
aggMaskingMode: "MaskAllText",
|
|
380
|
+
aggConsentState: "Granted",
|
|
381
|
+
aggRecorderKind: "dom",
|
|
382
|
+
aggRecorderVersion: "1.0.0",
|
|
383
|
+
aggRrwebVersion: "2.1.1",
|
|
384
|
+
aggSchemaVersion: 1,
|
|
385
|
+
aggWireVersion: 1,
|
|
386
|
+
aggEntryUrl: "https://example.com/",
|
|
387
|
+
aggExitUrl: "https://example.com/checkout",
|
|
388
|
+
aggRoutes: ["/", "/checkout"],
|
|
389
|
+
aggBrowserName: "Chrome",
|
|
390
|
+
aggBrowserVersion: "120",
|
|
391
|
+
aggOsName: "macOS",
|
|
392
|
+
aggDeviceType: "desktop",
|
|
393
|
+
aggCountryCode: "GB",
|
|
394
|
+
aggViewportWidth: 1440,
|
|
395
|
+
aggViewportHeight: 900,
|
|
396
|
+
aggFidelityNotices: ["canvas-not-recorded"],
|
|
397
|
+
aggFullSnapshotChunkIndexes: [0, 2],
|
|
398
|
+
aggTraceIds: [],
|
|
399
|
+
aggExceptionFingerprints: ["fp-1"],
|
|
400
|
+
aggClockSkewMs: 12,
|
|
401
|
+
aggIdentifiedUserKey: "hmac-key",
|
|
402
|
+
aggSamplePercentage: 10,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
describe("Session replay playback API", () => {
|
|
407
|
+
let projectId: ObjectID;
|
|
408
|
+
let userId: ObjectID;
|
|
409
|
+
let applicationAId: ObjectID;
|
|
410
|
+
let applicationBId: ObjectID;
|
|
411
|
+
let labelAId: ObjectID;
|
|
412
|
+
let labelBId: ObjectID;
|
|
413
|
+
|
|
414
|
+
let headerQuerySpy: jest.SpyInstance;
|
|
415
|
+
let chunkQuerySpy: jest.SpyInstance;
|
|
416
|
+
let findOneBySpy: jest.SpyInstance;
|
|
417
|
+
let findBySpy: jest.SpyInstance;
|
|
418
|
+
let recordViewSpy: jest.SpyInstance;
|
|
419
|
+
let recordSecondsWatchedSpy: jest.SpyInstance;
|
|
420
|
+
let viewFindOneBySpy: jest.SpyInstance;
|
|
421
|
+
|
|
422
|
+
beforeAll(() => {
|
|
423
|
+
recordedRoutes.length = 0;
|
|
424
|
+
/*
|
|
425
|
+
* Loaded lazily rather than with a top-level import. TelemetryAPI
|
|
426
|
+
* calls Express.getRouter() at module scope, so a static import would
|
|
427
|
+
* run the mock factory during the import phase - before the const
|
|
428
|
+
* router above is initialised - and die in the temporal dead zone.
|
|
429
|
+
*/
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
431
|
+
require("../../../Server/API/TelemetryAPI");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
beforeEach(() => {
|
|
435
|
+
jest.clearAllMocks();
|
|
436
|
+
|
|
437
|
+
projectId = ObjectID.generate();
|
|
438
|
+
userId = ObjectID.generate();
|
|
439
|
+
applicationAId = ObjectID.generate();
|
|
440
|
+
applicationBId = ObjectID.generate();
|
|
441
|
+
labelAId = ObjectID.generate();
|
|
442
|
+
labelBId = ObjectID.generate();
|
|
443
|
+
|
|
444
|
+
headerQuerySpy = jest
|
|
445
|
+
.spyOn(RumSessionService, "executeQuery")
|
|
446
|
+
.mockResolvedValue(fakeResultSet([]) as never);
|
|
447
|
+
|
|
448
|
+
chunkQuerySpy = jest
|
|
449
|
+
.spyOn(RumSessionChunkService, "executeQuery")
|
|
450
|
+
.mockResolvedValue(fakeResultSet([]) as never);
|
|
451
|
+
|
|
452
|
+
findOneBySpy = jest.spyOn(RumApplicationService, "findOneBy");
|
|
453
|
+
findBySpy = jest.spyOn(RumApplicationService, "findBy");
|
|
454
|
+
|
|
455
|
+
recordViewSpy = jest.spyOn(RumSessionReplayViewService, "recordView");
|
|
456
|
+
recordSecondsWatchedSpy = jest
|
|
457
|
+
.spyOn(RumSessionReplayViewService, "recordSecondsWatched")
|
|
458
|
+
.mockResolvedValue(undefined);
|
|
459
|
+
viewFindOneBySpy = jest
|
|
460
|
+
.spyOn(RumSessionReplayViewService, "findOneBy")
|
|
461
|
+
.mockResolvedValue(null);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
afterEach(() => {
|
|
465
|
+
jest.restoreAllMocks();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function mockProps(props: DatabaseCommonInteractionProps): void {
|
|
469
|
+
jest
|
|
470
|
+
.spyOn(CommonAPI, "getDatabaseCommonInteractionProps")
|
|
471
|
+
.mockResolvedValue(props);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function mockApplication(data: {
|
|
475
|
+
id: ObjectID;
|
|
476
|
+
labelIds: Array<ObjectID>;
|
|
477
|
+
}): void {
|
|
478
|
+
const application: RumApplication = new RumApplication();
|
|
479
|
+
application.id = data.id;
|
|
480
|
+
application.projectId = projectId;
|
|
481
|
+
application.labels = data.labelIds.map((labelId: ObjectID): Label => {
|
|
482
|
+
const label: Label = new Label();
|
|
483
|
+
label.id = labelId;
|
|
484
|
+
return label;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
findOneBySpy.mockResolvedValue(application);
|
|
488
|
+
findBySpy.mockResolvedValue([application]);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function mockSessionHeader(rumApplicationId: ObjectID): void {
|
|
492
|
+
headerQuerySpy.mockResolvedValue(
|
|
493
|
+
fakeResultSet([
|
|
494
|
+
buildHeaderRow({
|
|
495
|
+
sessionId: "session-1",
|
|
496
|
+
projectId: projectId,
|
|
497
|
+
rumApplicationId: rumApplicationId,
|
|
498
|
+
}),
|
|
499
|
+
]) as never,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/*
|
|
504
|
+
* A view row the CALLER owns. The handler queries with viewedByUserId in
|
|
505
|
+
* the predicate, so this stands in for "the lookup matched".
|
|
506
|
+
*/
|
|
507
|
+
function mockOwnView(data: {
|
|
508
|
+
viewId: ObjectID;
|
|
509
|
+
rumApplicationId: ObjectID;
|
|
510
|
+
}): void {
|
|
511
|
+
const view: RumSessionReplayView = new RumSessionReplayView();
|
|
512
|
+
view.id = data.viewId;
|
|
513
|
+
view.projectId = projectId;
|
|
514
|
+
view.rumApplicationId = data.rumApplicationId;
|
|
515
|
+
view.viewedByUserId = userId;
|
|
516
|
+
|
|
517
|
+
viewFindOneBySpy.mockResolvedValue(view);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function mockRecordedView(): ObjectID {
|
|
521
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
522
|
+
const view: RumSessionReplayView = new RumSessionReplayView();
|
|
523
|
+
view.id = viewId;
|
|
524
|
+
recordViewSpy.mockResolvedValue(view);
|
|
525
|
+
return viewId;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
describe("guard shape", () => {
|
|
529
|
+
test("all six routes are registered and every one carries the three-middleware guard", () => {
|
|
530
|
+
for (const uri of [
|
|
531
|
+
LIST_ROUTE,
|
|
532
|
+
MANIFEST_ROUTE,
|
|
533
|
+
CHUNKS_ROUTE,
|
|
534
|
+
HEARTBEAT_ROUTE,
|
|
535
|
+
FOR_EXCEPTION_ROUTE,
|
|
536
|
+
INGEST_STATUS_ROUTE,
|
|
537
|
+
]) {
|
|
538
|
+
const route: RecordedRoute = findRoute(uri);
|
|
539
|
+
|
|
540
|
+
expect(route.handlers).toHaveLength(4);
|
|
541
|
+
expect(route.handlers[0]).toBe(UserMiddleware.getUserMiddleware);
|
|
542
|
+
expect(route.handlers[1]).toBe(
|
|
543
|
+
UserMiddleware.requireUserAuthentication,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
/*
|
|
549
|
+
* handlers[2] is the permission guard itself, and swapping the payload
|
|
550
|
+
* guard for the list guard on /manifest or /chunks would leave the
|
|
551
|
+
* shape assertion above entirely green. The three payload routes must
|
|
552
|
+
* share ONE guard instance and the two list routes another, and the
|
|
553
|
+
* two must not be the same object.
|
|
554
|
+
*/
|
|
555
|
+
test("the payload routes and the list routes carry different permission guards", () => {
|
|
556
|
+
const payloadGuard: RouterFunction | undefined =
|
|
557
|
+
findRoute(MANIFEST_ROUTE).handlers[2];
|
|
558
|
+
const listGuard: RouterFunction | undefined =
|
|
559
|
+
findRoute(LIST_ROUTE).handlers[2];
|
|
560
|
+
|
|
561
|
+
expect(payloadGuard).toBeDefined();
|
|
562
|
+
expect(listGuard).toBeDefined();
|
|
563
|
+
expect(payloadGuard).not.toBe(listGuard);
|
|
564
|
+
|
|
565
|
+
for (const uri of [MANIFEST_ROUTE, CHUNKS_ROUTE, HEARTBEAT_ROUTE]) {
|
|
566
|
+
expect(findRoute(uri).handlers[2]).toBe(payloadGuard);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
expect(findRoute(FOR_EXCEPTION_ROUTE).handlers[2]).toBe(listGuard);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
/*
|
|
574
|
+
* The chunk table's replace key is (projectId, sessionId, tabId,
|
|
575
|
+
* chunkIndex) - rumApplicationId is a plain column. sessionId is minted
|
|
576
|
+
* by the browser, so two applications can share one. Authorizing an
|
|
577
|
+
* application and then reading on (projectId, sessionId) alone is a
|
|
578
|
+
* cross-application disclosure.
|
|
579
|
+
*/
|
|
580
|
+
describe("chunk reads are pinned to the authorized application", () => {
|
|
581
|
+
test("the manifest query filters on the application resolved from the header", async () => {
|
|
582
|
+
const principal: {
|
|
583
|
+
request: JSONObject;
|
|
584
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
585
|
+
} = buildPrincipal({
|
|
586
|
+
projectId: projectId,
|
|
587
|
+
userId: userId,
|
|
588
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
mockProps(principal.databaseProps);
|
|
592
|
+
mockSessionHeader(applicationAId);
|
|
593
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
594
|
+
mockRecordedView();
|
|
595
|
+
|
|
596
|
+
await callRoute({
|
|
597
|
+
uri: MANIFEST_ROUTE,
|
|
598
|
+
request: principal.request,
|
|
599
|
+
body: { sessionId: "session-1" },
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
603
|
+
|
|
604
|
+
expect(statement.query).toContain("rumApplicationId = ");
|
|
605
|
+
expect(Object.values(statement.query_params)).toContain(
|
|
606
|
+
applicationAId.toString(),
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("both chunk queries filter on the application resolved from the header", async () => {
|
|
611
|
+
const principal: {
|
|
612
|
+
request: JSONObject;
|
|
613
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
614
|
+
} = buildPrincipal({
|
|
615
|
+
projectId: projectId,
|
|
616
|
+
userId: userId,
|
|
617
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
mockProps(principal.databaseProps);
|
|
621
|
+
mockSessionHeader(applicationAId);
|
|
622
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
623
|
+
|
|
624
|
+
chunkQuerySpy
|
|
625
|
+
.mockResolvedValueOnce(
|
|
626
|
+
fakeResultSet([{ chunkIndex: 0, chunkStoredBytes: 3 }]) as never,
|
|
627
|
+
)
|
|
628
|
+
.mockResolvedValueOnce(
|
|
629
|
+
fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
await callRoute({
|
|
633
|
+
uri: CHUNKS_ROUTE,
|
|
634
|
+
request: principal.request,
|
|
635
|
+
body: {
|
|
636
|
+
sessionId: "session-1",
|
|
637
|
+
tabId: "tab-1",
|
|
638
|
+
chunkIndexes: [0],
|
|
639
|
+
/* A caller-supplied application id must change nothing. */
|
|
640
|
+
rumApplicationId: applicationBId.toString(),
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(2);
|
|
645
|
+
|
|
646
|
+
for (const call of chunkQuerySpy.mock.calls) {
|
|
647
|
+
const statement: Statement = call[0] as Statement;
|
|
648
|
+
expect(statement.query).toContain("rumApplicationId = ");
|
|
649
|
+
expect(Object.values(statement.query_params)).toContain(
|
|
650
|
+
applicationAId.toString(),
|
|
651
|
+
);
|
|
652
|
+
expect(Object.values(statement.query_params)).not.toContain(
|
|
653
|
+
applicationBId.toString(),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("a sessionId that exists under two applications is refused rather than resolved to the newest", async () => {
|
|
659
|
+
const principal: {
|
|
660
|
+
request: JSONObject;
|
|
661
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
662
|
+
} = buildPrincipal({
|
|
663
|
+
projectId: projectId,
|
|
664
|
+
userId: userId,
|
|
665
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
666
|
+
labelIds: [labelAId],
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
mockProps(principal.databaseProps);
|
|
670
|
+
|
|
671
|
+
/*
|
|
672
|
+
* Application A is the caller's own and sorts first (newest). Under
|
|
673
|
+
* the old LIMIT 1 the label check would pass on A and the chunk
|
|
674
|
+
* reads would then serve B's recording.
|
|
675
|
+
*/
|
|
676
|
+
headerQuerySpy.mockResolvedValue(
|
|
677
|
+
fakeResultSet([
|
|
678
|
+
buildHeaderRow({
|
|
679
|
+
sessionId: "session-1",
|
|
680
|
+
projectId: projectId,
|
|
681
|
+
rumApplicationId: applicationAId,
|
|
682
|
+
}),
|
|
683
|
+
buildHeaderRow({
|
|
684
|
+
sessionId: "session-1",
|
|
685
|
+
projectId: projectId,
|
|
686
|
+
rumApplicationId: applicationBId,
|
|
687
|
+
}),
|
|
688
|
+
]) as never,
|
|
689
|
+
);
|
|
690
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
691
|
+
mockRecordedView();
|
|
692
|
+
|
|
693
|
+
const result: CallResult = await callRoute({
|
|
694
|
+
uri: MANIFEST_ROUTE,
|
|
695
|
+
request: principal.request,
|
|
696
|
+
body: { sessionId: "session-1" },
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
700
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
701
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
/*
|
|
706
|
+
* PermissionScope.Owned is excluded from getNonAccessControlPermissions
|
|
707
|
+
* AND from getAccessControlPermissions, because the ORM enforces it
|
|
708
|
+
* separately via OwnedScopePermission. This bespoke path has no such
|
|
709
|
+
* step, so an Owned grant must be refused, never treated as unscoped.
|
|
710
|
+
*/
|
|
711
|
+
describe("PermissionScope.Owned fails closed", () => {
|
|
712
|
+
test("an Owned-scoped payload grant cannot read a recording", async () => {
|
|
713
|
+
const principal: {
|
|
714
|
+
request: JSONObject;
|
|
715
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
716
|
+
} = buildPrincipal({
|
|
717
|
+
projectId: projectId,
|
|
718
|
+
userId: userId,
|
|
719
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
720
|
+
scope: PermissionScope.Owned,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
mockProps(principal.databaseProps);
|
|
724
|
+
mockSessionHeader(applicationAId);
|
|
725
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
726
|
+
mockRecordedView();
|
|
727
|
+
|
|
728
|
+
const result: CallResult = await callRoute({
|
|
729
|
+
uri: MANIFEST_ROUTE,
|
|
730
|
+
request: principal.request,
|
|
731
|
+
body: { sessionId: "session-1" },
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
735
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
736
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("an Owned-scoped list grant cannot read the whole project's exceptions", async () => {
|
|
740
|
+
const principal: {
|
|
741
|
+
request: JSONObject;
|
|
742
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
743
|
+
} = buildPrincipal({
|
|
744
|
+
projectId: projectId,
|
|
745
|
+
userId: userId,
|
|
746
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
747
|
+
scope: PermissionScope.Owned,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
mockProps(principal.databaseProps);
|
|
751
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
752
|
+
|
|
753
|
+
const result: CallResult = await callRoute({
|
|
754
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
755
|
+
request: principal.request,
|
|
756
|
+
body: { fingerprint: "fp-1" },
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
760
|
+
/* Above all: no unfiltered project-wide query was issued. */
|
|
761
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("an Owned-scoped grant cannot list an application's sessions", async () => {
|
|
765
|
+
const principal: {
|
|
766
|
+
request: JSONObject;
|
|
767
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
768
|
+
} = buildPrincipal({
|
|
769
|
+
projectId: projectId,
|
|
770
|
+
userId: userId,
|
|
771
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
772
|
+
scope: PermissionScope.Owned,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
mockProps(principal.databaseProps);
|
|
776
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
777
|
+
|
|
778
|
+
const result: CallResult = await callRoute({
|
|
779
|
+
uri: LIST_ROUTE,
|
|
780
|
+
request: principal.request,
|
|
781
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
785
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* The core of the design: the payload guard must NOT be the OR-list
|
|
791
|
+
* createTelemetryReadAccessGuard builds, which already contains
|
|
792
|
+
* ProjectMember, Viewer and TelemetryViewer. If someone ever
|
|
793
|
+
* "simplifies" these routes onto that helper, these four cases fail.
|
|
794
|
+
*/
|
|
795
|
+
describe("payload routes reject the roles createTelemetryReadAccessGuard would have admitted", () => {
|
|
796
|
+
const deniedRoles: Array<{ name: string; permission: Permission }> = [
|
|
797
|
+
{ name: "Viewer", permission: Permission.Viewer },
|
|
798
|
+
{ name: "ProjectMember", permission: Permission.ProjectMember },
|
|
799
|
+
{ name: "TelemetryViewer", permission: Permission.TelemetryViewer },
|
|
800
|
+
{ name: "TelemetryMember", permission: Permission.TelemetryMember },
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
test.each(deniedRoles)(
|
|
804
|
+
"a $name-only principal is denied /manifest",
|
|
805
|
+
async (role: { name: string; permission: Permission }) => {
|
|
806
|
+
const principal: {
|
|
807
|
+
request: JSONObject;
|
|
808
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
809
|
+
} = buildPrincipal({
|
|
810
|
+
projectId: projectId,
|
|
811
|
+
userId: userId,
|
|
812
|
+
permissions: [role.permission],
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
mockProps(principal.databaseProps);
|
|
816
|
+
mockSessionHeader(applicationAId);
|
|
817
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
818
|
+
mockRecordedView();
|
|
819
|
+
|
|
820
|
+
const result: CallResult = await callRoute({
|
|
821
|
+
uri: MANIFEST_ROUTE,
|
|
822
|
+
request: principal.request,
|
|
823
|
+
body: { sessionId: "session-1" },
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
827
|
+
expect(result.deniedWith?.code).toBe(422);
|
|
828
|
+
expect(result.reachedHandler).toBe(false);
|
|
829
|
+
// Nothing was read, and no audit row claims a view that never happened.
|
|
830
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
831
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
832
|
+
},
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
test.each(deniedRoles)(
|
|
836
|
+
"a $name-only principal is denied /chunks",
|
|
837
|
+
async (role: { name: string; permission: Permission }) => {
|
|
838
|
+
const principal: {
|
|
839
|
+
request: JSONObject;
|
|
840
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
841
|
+
} = buildPrincipal({
|
|
842
|
+
projectId: projectId,
|
|
843
|
+
userId: userId,
|
|
844
|
+
permissions: [role.permission],
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
mockProps(principal.databaseProps);
|
|
848
|
+
mockSessionHeader(applicationAId);
|
|
849
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
850
|
+
|
|
851
|
+
const result: CallResult = await callRoute({
|
|
852
|
+
uri: CHUNKS_ROUTE,
|
|
853
|
+
request: principal.request,
|
|
854
|
+
body: {
|
|
855
|
+
sessionId: "session-1",
|
|
856
|
+
tabId: "tab-1",
|
|
857
|
+
chunkIndexes: [0],
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
862
|
+
expect(result.deniedWith?.code).toBe(422);
|
|
863
|
+
expect(result.reachedHandler).toBe(false);
|
|
864
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
test("ReadRumSessionReplay alone is not enough to watch a recording", async () => {
|
|
869
|
+
const principal: {
|
|
870
|
+
request: JSONObject;
|
|
871
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
872
|
+
} = buildPrincipal({
|
|
873
|
+
projectId: projectId,
|
|
874
|
+
userId: userId,
|
|
875
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
mockProps(principal.databaseProps);
|
|
879
|
+
mockSessionHeader(applicationAId);
|
|
880
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
881
|
+
|
|
882
|
+
const result: CallResult = await callRoute({
|
|
883
|
+
uri: MANIFEST_ROUTE,
|
|
884
|
+
request: principal.request,
|
|
885
|
+
body: { sessionId: "session-1" },
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
889
|
+
expect(result.reachedHandler).toBe(false);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("ReadRumSessionReplayPayload alone reaches the manifest handler", async () => {
|
|
893
|
+
const principal: {
|
|
894
|
+
request: JSONObject;
|
|
895
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
896
|
+
} = buildPrincipal({
|
|
897
|
+
projectId: projectId,
|
|
898
|
+
userId: userId,
|
|
899
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
mockProps(principal.databaseProps);
|
|
903
|
+
mockSessionHeader(applicationAId);
|
|
904
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
905
|
+
mockRecordedView();
|
|
906
|
+
|
|
907
|
+
const result: CallResult = await callRoute({
|
|
908
|
+
uri: MANIFEST_ROUTE,
|
|
909
|
+
request: principal.request,
|
|
910
|
+
body: { sessionId: "session-1" },
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
expect(result.deniedWith).toBeUndefined();
|
|
914
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
915
|
+
expect(result.reachedHandler).toBe(true);
|
|
916
|
+
expect(result.jsonBody).toBeDefined();
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
describe("application scope", () => {
|
|
921
|
+
test("a caller scoped to application A is refused a session in application B", async () => {
|
|
922
|
+
const principal: {
|
|
923
|
+
request: JSONObject;
|
|
924
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
925
|
+
} = buildPrincipal({
|
|
926
|
+
projectId: projectId,
|
|
927
|
+
userId: userId,
|
|
928
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
929
|
+
labelIds: [labelAId],
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
mockProps(principal.databaseProps);
|
|
933
|
+
// The session belongs to application B, which carries a different label.
|
|
934
|
+
mockSessionHeader(applicationBId);
|
|
935
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
936
|
+
mockRecordedView();
|
|
937
|
+
|
|
938
|
+
const result: CallResult = await callRoute({
|
|
939
|
+
uri: MANIFEST_ROUTE,
|
|
940
|
+
request: principal.request,
|
|
941
|
+
body: { sessionId: "session-1" },
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
945
|
+
expect((result.thrownToNext as Exception).code).toBe(422);
|
|
946
|
+
// The audit row is written only after authorization succeeds.
|
|
947
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("the owning application is resolved from the session header, never from the request body", async () => {
|
|
951
|
+
const principal: {
|
|
952
|
+
request: JSONObject;
|
|
953
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
954
|
+
} = buildPrincipal({
|
|
955
|
+
projectId: projectId,
|
|
956
|
+
userId: userId,
|
|
957
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
958
|
+
labelIds: [labelAId],
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
mockProps(principal.databaseProps);
|
|
962
|
+
mockSessionHeader(applicationBId);
|
|
963
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
964
|
+
mockRecordedView();
|
|
965
|
+
|
|
966
|
+
const result: CallResult = await callRoute({
|
|
967
|
+
uri: MANIFEST_ROUTE,
|
|
968
|
+
request: principal.request,
|
|
969
|
+
body: {
|
|
970
|
+
sessionId: "session-1",
|
|
971
|
+
// A caller-supplied application id must be ignored entirely.
|
|
972
|
+
rumApplicationId: applicationAId.toString(),
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
977
|
+
|
|
978
|
+
const lookupArgs: JSONObject = findOneBySpy.mock
|
|
979
|
+
.calls[0]![0] as JSONObject;
|
|
980
|
+
const query: JSONObject = lookupArgs["query"] as JSONObject;
|
|
981
|
+
expect(query["_id"]).toBe(applicationBId.toString());
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
test("a caller whose label matches the owning application is allowed", async () => {
|
|
985
|
+
const principal: {
|
|
986
|
+
request: JSONObject;
|
|
987
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
988
|
+
} = buildPrincipal({
|
|
989
|
+
projectId: projectId,
|
|
990
|
+
userId: userId,
|
|
991
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
992
|
+
labelIds: [labelAId],
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
mockProps(principal.databaseProps);
|
|
996
|
+
mockSessionHeader(applicationAId);
|
|
997
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId, labelBId] });
|
|
998
|
+
mockRecordedView();
|
|
999
|
+
|
|
1000
|
+
const result: CallResult = await callRoute({
|
|
1001
|
+
uri: MANIFEST_ROUTE,
|
|
1002
|
+
request: principal.request,
|
|
1003
|
+
body: { sessionId: "session-1" },
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(result.thrownToNext).toBeUndefined();
|
|
1007
|
+
expect(result.deniedWith).toBeUndefined();
|
|
1008
|
+
expect(recordViewSpy).toHaveBeenCalledTimes(1);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("the tenant comes from databaseProps, not from the body", async () => {
|
|
1012
|
+
const principal: {
|
|
1013
|
+
request: JSONObject;
|
|
1014
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1015
|
+
} = buildPrincipal({
|
|
1016
|
+
projectId: projectId,
|
|
1017
|
+
userId: userId,
|
|
1018
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
mockProps(principal.databaseProps);
|
|
1022
|
+
mockSessionHeader(applicationAId);
|
|
1023
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1024
|
+
mockRecordedView();
|
|
1025
|
+
|
|
1026
|
+
const otherProjectId: ObjectID = ObjectID.generate();
|
|
1027
|
+
|
|
1028
|
+
await callRoute({
|
|
1029
|
+
uri: MANIFEST_ROUTE,
|
|
1030
|
+
request: principal.request,
|
|
1031
|
+
body: {
|
|
1032
|
+
sessionId: "session-1",
|
|
1033
|
+
projectId: otherProjectId.toString(),
|
|
1034
|
+
tenantId: otherProjectId.toString(),
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1039
|
+
.calls[0]![0] as Statement;
|
|
1040
|
+
const params: Record<string, unknown> = statement.query_params;
|
|
1041
|
+
|
|
1042
|
+
expect(Object.values(params)).toContain(projectId.toString());
|
|
1043
|
+
expect(Object.values(params)).not.toContain(otherProjectId.toString());
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
describe("list query", () => {
|
|
1048
|
+
test("de-duplicates ReplacingMergeTree rows with argMax over the session identity", async () => {
|
|
1049
|
+
const principal: {
|
|
1050
|
+
request: JSONObject;
|
|
1051
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1052
|
+
} = buildPrincipal({
|
|
1053
|
+
projectId: projectId,
|
|
1054
|
+
userId: userId,
|
|
1055
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
mockProps(principal.databaseProps);
|
|
1059
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1060
|
+
|
|
1061
|
+
await callRoute({
|
|
1062
|
+
uri: LIST_ROUTE,
|
|
1063
|
+
request: principal.request,
|
|
1064
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
expect(headerQuerySpy).toHaveBeenCalledTimes(1);
|
|
1068
|
+
|
|
1069
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1070
|
+
.calls[0]![0] as Statement;
|
|
1071
|
+
const query: string = statement.query;
|
|
1072
|
+
|
|
1073
|
+
expect(query).toContain("argMax(");
|
|
1074
|
+
expect(query).toContain("argMax(startTime, version)");
|
|
1075
|
+
expect(query).toContain(
|
|
1076
|
+
"GROUP BY projectId, rumApplicationId, sessionId",
|
|
1077
|
+
);
|
|
1078
|
+
// No FINAL support exists in this repo; it must never appear.
|
|
1079
|
+
expect(query).not.toContain(" FINAL");
|
|
1080
|
+
// A read must never resurrect rows past their retention date.
|
|
1081
|
+
expect(query).toContain("retentionDate >= now()");
|
|
1082
|
+
/*
|
|
1083
|
+
* 'break' silently returns partial results; the whole point of not
|
|
1084
|
+
* using BaseAnalyticsAPI is to avoid that.
|
|
1085
|
+
*/
|
|
1086
|
+
expect(query).toContain("timeout_overflow_mode = 'throw'");
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
test("omits identifiedUserLabel for a caller without the narrower identity permission", async () => {
|
|
1090
|
+
const principal: {
|
|
1091
|
+
request: JSONObject;
|
|
1092
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1093
|
+
} = buildPrincipal({
|
|
1094
|
+
projectId: projectId,
|
|
1095
|
+
userId: userId,
|
|
1096
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
mockProps(principal.databaseProps);
|
|
1100
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1101
|
+
|
|
1102
|
+
await callRoute({
|
|
1103
|
+
uri: LIST_ROUTE,
|
|
1104
|
+
request: principal.request,
|
|
1105
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1109
|
+
.calls[0]![0] as Statement;
|
|
1110
|
+
|
|
1111
|
+
expect(statement.query).not.toContain("identifiedUserLabel");
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
/*
|
|
1115
|
+
* The strictest column in the schema. A caller can hold
|
|
1116
|
+
* ReadRumSessionReplay unscoped (so the list route admits them for
|
|
1117
|
+
* every application) while their identity grant -
|
|
1118
|
+
* ReadRumSessionReplayPayload - is scoped to one label. Checking only
|
|
1119
|
+
* that the permission NAME is present hands them named end users for
|
|
1120
|
+
* applications outside that label.
|
|
1121
|
+
*/
|
|
1122
|
+
test("omits identifiedUserLabel when the identity grant is scoped to a different application", async () => {
|
|
1123
|
+
const principal: {
|
|
1124
|
+
request: JSONObject;
|
|
1125
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1126
|
+
} = buildPrincipal({
|
|
1127
|
+
projectId: projectId,
|
|
1128
|
+
userId: userId,
|
|
1129
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1130
|
+
labelIds: [labelAId],
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
grantUnscopedListPermission(principal.databaseProps, projectId);
|
|
1134
|
+
|
|
1135
|
+
mockProps(principal.databaseProps);
|
|
1136
|
+
/* Application B is outside the identity grant's label. */
|
|
1137
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
1138
|
+
|
|
1139
|
+
await callRoute({
|
|
1140
|
+
uri: LIST_ROUTE,
|
|
1141
|
+
request: principal.request,
|
|
1142
|
+
body: { rumApplicationId: applicationBId.toString() },
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1146
|
+
.calls[0]![0] as Statement;
|
|
1147
|
+
|
|
1148
|
+
expect(statement.query).not.toContain("identifiedUserLabel");
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
test("selects identifiedUserLabel when the identity grant covers this application", async () => {
|
|
1152
|
+
const principal: {
|
|
1153
|
+
request: JSONObject;
|
|
1154
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1155
|
+
} = buildPrincipal({
|
|
1156
|
+
projectId: projectId,
|
|
1157
|
+
userId: userId,
|
|
1158
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1159
|
+
labelIds: [labelAId],
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
grantUnscopedListPermission(principal.databaseProps, projectId);
|
|
1163
|
+
|
|
1164
|
+
mockProps(principal.databaseProps);
|
|
1165
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
1166
|
+
|
|
1167
|
+
await callRoute({
|
|
1168
|
+
uri: LIST_ROUTE,
|
|
1169
|
+
request: principal.request,
|
|
1170
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1174
|
+
.calls[0]![0] as Statement;
|
|
1175
|
+
|
|
1176
|
+
expect(statement.query).toContain(
|
|
1177
|
+
"argMax(identifiedUserLabel, version) AS aggIdentifiedUserLabel",
|
|
1178
|
+
);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
test("a malformed rumApplicationId is a bad request, not a database error", async () => {
|
|
1182
|
+
const principal: {
|
|
1183
|
+
request: JSONObject;
|
|
1184
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1185
|
+
} = buildPrincipal({
|
|
1186
|
+
projectId: projectId,
|
|
1187
|
+
userId: userId,
|
|
1188
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
mockProps(principal.databaseProps);
|
|
1192
|
+
|
|
1193
|
+
const result: CallResult = await callRoute({
|
|
1194
|
+
uri: LIST_ROUTE,
|
|
1195
|
+
request: principal.request,
|
|
1196
|
+
body: { rumApplicationId: "nope" },
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1200
|
+
expect(findOneBySpy).not.toHaveBeenCalled();
|
|
1201
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
test("selects identifiedUserLabel for a ProjectAdmin", async () => {
|
|
1205
|
+
const principal: {
|
|
1206
|
+
request: JSONObject;
|
|
1207
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1208
|
+
} = buildPrincipal({
|
|
1209
|
+
projectId: projectId,
|
|
1210
|
+
userId: userId,
|
|
1211
|
+
permissions: [Permission.ProjectAdmin],
|
|
1212
|
+
});
|
|
1213
|
+
|
|
1214
|
+
mockProps(principal.databaseProps);
|
|
1215
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1216
|
+
|
|
1217
|
+
await callRoute({
|
|
1218
|
+
uri: LIST_ROUTE,
|
|
1219
|
+
request: principal.request,
|
|
1220
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1224
|
+
.calls[0]![0] as Statement;
|
|
1225
|
+
|
|
1226
|
+
expect(statement.query).toContain(
|
|
1227
|
+
"argMax(identifiedUserLabel, version) AS aggIdentifiedUserLabel",
|
|
1228
|
+
);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test("the frustration filter is applied SERVER-side, over the argMax aliases", async () => {
|
|
1232
|
+
const principal: {
|
|
1233
|
+
request: JSONObject;
|
|
1234
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1235
|
+
} = buildPrincipal({
|
|
1236
|
+
projectId: projectId,
|
|
1237
|
+
userId: userId,
|
|
1238
|
+
permissions: [Permission.ProjectAdmin],
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
mockProps(principal.databaseProps);
|
|
1242
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1243
|
+
|
|
1244
|
+
await callRoute({
|
|
1245
|
+
uri: LIST_ROUTE,
|
|
1246
|
+
request: principal.request,
|
|
1247
|
+
body: {
|
|
1248
|
+
rumApplicationId: applicationAId.toString(),
|
|
1249
|
+
filters: { hasFrustration: true },
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1254
|
+
.calls[0]![0] as Statement;
|
|
1255
|
+
|
|
1256
|
+
/*
|
|
1257
|
+
* The old client-side version filtered only the fetched page, so a
|
|
1258
|
+
* project whose frustrated sessions sat on page 2 showed an empty
|
|
1259
|
+
* list under a label that promised otherwise. The aliases matter
|
|
1260
|
+
* too: raw columns would sum across ReplacingMergeTree versions.
|
|
1261
|
+
*/
|
|
1262
|
+
expect(statement.query).toContain(
|
|
1263
|
+
"(aggRageClickCount + aggDeadClickCount + aggErrorClickCount + aggRefreshRageCount) > 0",
|
|
1264
|
+
);
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
describe("manifest", () => {
|
|
1269
|
+
test("never names the payload column and writes the read audit row", async () => {
|
|
1270
|
+
const principal: {
|
|
1271
|
+
request: JSONObject;
|
|
1272
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1273
|
+
} = buildPrincipal({
|
|
1274
|
+
projectId: projectId,
|
|
1275
|
+
userId: userId,
|
|
1276
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
mockProps(principal.databaseProps);
|
|
1280
|
+
mockSessionHeader(applicationAId);
|
|
1281
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1282
|
+
const viewId: ObjectID = mockRecordedView();
|
|
1283
|
+
|
|
1284
|
+
chunkQuerySpy.mockResolvedValue(
|
|
1285
|
+
fakeResultSet([
|
|
1286
|
+
{
|
|
1287
|
+
tabId: "tab-1",
|
|
1288
|
+
chunkIndex: 0,
|
|
1289
|
+
chunkStartOffsetMs: 0,
|
|
1290
|
+
chunkEndOffsetMs: 15000,
|
|
1291
|
+
eventCount: 100,
|
|
1292
|
+
hasFullSnapshot: true,
|
|
1293
|
+
chunkPayloadBytes: 1024,
|
|
1294
|
+
errorCount: 2,
|
|
1295
|
+
rageClickCount: 1,
|
|
1296
|
+
deadClickCount: 4,
|
|
1297
|
+
errorClickCount: 6,
|
|
1298
|
+
refreshRageCount: 5,
|
|
1299
|
+
routeCount: 3,
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
tabId: "tab-1",
|
|
1303
|
+
chunkIndex: 3,
|
|
1304
|
+
chunkStartOffsetMs: 45000,
|
|
1305
|
+
chunkEndOffsetMs: 60000,
|
|
1306
|
+
eventCount: 100,
|
|
1307
|
+
hasFullSnapshot: false,
|
|
1308
|
+
chunkPayloadBytes: 1024,
|
|
1309
|
+
errorCount: 0,
|
|
1310
|
+
rageClickCount: 0,
|
|
1311
|
+
deadClickCount: 0,
|
|
1312
|
+
errorClickCount: 0,
|
|
1313
|
+
refreshRageCount: 0,
|
|
1314
|
+
routeCount: 0,
|
|
1315
|
+
},
|
|
1316
|
+
]) as never,
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
const result: CallResult = await callRoute({
|
|
1320
|
+
uri: MANIFEST_ROUTE,
|
|
1321
|
+
request: principal.request,
|
|
1322
|
+
body: { sessionId: "session-1", accessReason: "incident triage" },
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
const statement: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
1326
|
+
|
|
1327
|
+
expect(statement.query).not.toContain("payload,");
|
|
1328
|
+
expect(statement.query).not.toMatch(/\bpayload\b(?!Bytes)/);
|
|
1329
|
+
expect(statement.query).toContain("LIMIT 1 BY tabId, chunkIndex");
|
|
1330
|
+
|
|
1331
|
+
/*
|
|
1332
|
+
* The signal counters must be projected: the player's error,
|
|
1333
|
+
* frustration and route timeline lanes and the "next error" jump are
|
|
1334
|
+
* fed exclusively by these per-chunk columns. Dropping one from the
|
|
1335
|
+
* SELECT silently blanks a lane — it parses as 0 client-side.
|
|
1336
|
+
*/
|
|
1337
|
+
for (const counterColumn of [
|
|
1338
|
+
"errorCount",
|
|
1339
|
+
"rageClickCount",
|
|
1340
|
+
"deadClickCount",
|
|
1341
|
+
"errorClickCount",
|
|
1342
|
+
"refreshRageCount",
|
|
1343
|
+
"routeCount",
|
|
1344
|
+
]) {
|
|
1345
|
+
expect(statement.query).toContain(counterColumn);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
expect(recordViewSpy).toHaveBeenCalledTimes(1);
|
|
1349
|
+
const auditArgs: JSONObject = recordViewSpy.mock
|
|
1350
|
+
.calls[0]![0] as JSONObject;
|
|
1351
|
+
expect((auditArgs["projectId"] as ObjectID).toString()).toBe(
|
|
1352
|
+
projectId.toString(),
|
|
1353
|
+
);
|
|
1354
|
+
expect((auditArgs["rumApplicationId"] as ObjectID).toString()).toBe(
|
|
1355
|
+
applicationAId.toString(),
|
|
1356
|
+
);
|
|
1357
|
+
expect(auditArgs["sessionId"]).toBe("session-1");
|
|
1358
|
+
expect(auditArgs["accessReason"]).toBe("incident triage");
|
|
1359
|
+
expect(auditArgs["ipAddress"]).toBe("203.0.113.7");
|
|
1360
|
+
|
|
1361
|
+
const body: JSONObject = result.jsonBody as JSONObject;
|
|
1362
|
+
expect(body["viewId"]).toBe(viewId.toString());
|
|
1363
|
+
|
|
1364
|
+
const tabs: Array<JSONObject> = body[
|
|
1365
|
+
"tabs"
|
|
1366
|
+
] as unknown as Array<JSONObject>;
|
|
1367
|
+
expect(tabs).toHaveLength(1);
|
|
1368
|
+
expect(tabs[0]!["chunkIndexes"]).toEqual([0, 3]);
|
|
1369
|
+
expect(tabs[0]!["fullSnapshotChunkIndexes"]).toEqual([0]);
|
|
1370
|
+
|
|
1371
|
+
/*
|
|
1372
|
+
* Every counter gets a DISTINCT nonzero value, asserted individually:
|
|
1373
|
+
* the client parses missing keys to 0, so a counter dropped from the
|
|
1374
|
+
* SELECT (or two counters cross-wired in the row mapping) fails
|
|
1375
|
+
* loudly here instead of silently blanking a timeline lane.
|
|
1376
|
+
*/
|
|
1377
|
+
const manifestChunks: Array<JSONObject> = tabs[0]![
|
|
1378
|
+
"chunks"
|
|
1379
|
+
] as unknown as Array<JSONObject>;
|
|
1380
|
+
expect(manifestChunks[0]!["errorCount"]).toBe(2);
|
|
1381
|
+
expect(manifestChunks[0]!["rageClickCount"]).toBe(1);
|
|
1382
|
+
expect(manifestChunks[0]!["deadClickCount"]).toBe(4);
|
|
1383
|
+
expect(manifestChunks[0]!["errorClickCount"]).toBe(6);
|
|
1384
|
+
expect(manifestChunks[0]!["refreshRageCount"]).toBe(5);
|
|
1385
|
+
expect(manifestChunks[0]!["routeCount"]).toBe(3);
|
|
1386
|
+
expect(manifestChunks[1]!["errorCount"]).toBe(0);
|
|
1387
|
+
|
|
1388
|
+
/*
|
|
1389
|
+
* The hole between chunk 0 and chunk 3 must be reported. Playback
|
|
1390
|
+
* that silently crosses a gap renders a DOM the user never saw.
|
|
1391
|
+
*/
|
|
1392
|
+
const gaps: Array<JSONObject> = tabs[0]![
|
|
1393
|
+
"gaps"
|
|
1394
|
+
] as unknown as Array<JSONObject>;
|
|
1395
|
+
expect(gaps).toHaveLength(1);
|
|
1396
|
+
expect(gaps[0]!["fromIndex"]).toBe(0);
|
|
1397
|
+
expect(gaps[0]!["toIndex"]).toBe(3);
|
|
1398
|
+
expect(gaps[0]!["missingMs"]).toBe(30000);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
test("a session that does not exist in this tenant is refused before any audit row", async () => {
|
|
1402
|
+
const principal: {
|
|
1403
|
+
request: JSONObject;
|
|
1404
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1405
|
+
} = buildPrincipal({
|
|
1406
|
+
projectId: projectId,
|
|
1407
|
+
userId: userId,
|
|
1408
|
+
permissions: [Permission.ProjectOwner],
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
mockProps(principal.databaseProps);
|
|
1412
|
+
headerQuerySpy.mockResolvedValue(fakeResultSet([]) as never);
|
|
1413
|
+
|
|
1414
|
+
const result: CallResult = await callRoute({
|
|
1415
|
+
uri: MANIFEST_ROUTE,
|
|
1416
|
+
request: principal.request,
|
|
1417
|
+
body: { sessionId: "does-not-exist" },
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1421
|
+
expect(recordViewSpy).not.toHaveBeenCalled();
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
describe("chunks", () => {
|
|
1426
|
+
function payloadPrincipal(): {
|
|
1427
|
+
request: JSONObject;
|
|
1428
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1429
|
+
} {
|
|
1430
|
+
return buildPrincipal({
|
|
1431
|
+
projectId: projectId,
|
|
1432
|
+
userId: userId,
|
|
1433
|
+
permissions: [Permission.TelemetryAdmin],
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
test(`rejects more than ${MAX_SESSION_REPLAY_CHUNKS_PER_READ} chunks without touching ClickHouse`, async () => {
|
|
1438
|
+
const principal: {
|
|
1439
|
+
request: JSONObject;
|
|
1440
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1441
|
+
} = payloadPrincipal();
|
|
1442
|
+
|
|
1443
|
+
mockProps(principal.databaseProps);
|
|
1444
|
+
mockSessionHeader(applicationAId);
|
|
1445
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1446
|
+
|
|
1447
|
+
const tooManyIndexes: Array<number> = Array.from(
|
|
1448
|
+
{ length: MAX_SESSION_REPLAY_CHUNKS_PER_READ + 1 },
|
|
1449
|
+
(_unused: unknown, index: number): number => {
|
|
1450
|
+
return index;
|
|
1451
|
+
},
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
const result: CallResult = await callRoute({
|
|
1455
|
+
uri: CHUNKS_ROUTE,
|
|
1456
|
+
request: principal.request,
|
|
1457
|
+
body: {
|
|
1458
|
+
sessionId: "session-1",
|
|
1459
|
+
tabId: "tab-1",
|
|
1460
|
+
chunkIndexes: tooManyIndexes,
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1465
|
+
expect(result.deniedWith?.message).toContain(
|
|
1466
|
+
`${MAX_SESSION_REPLAY_CHUNKS_PER_READ}`,
|
|
1467
|
+
);
|
|
1468
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
1469
|
+
// The cap is checked before the session is even resolved.
|
|
1470
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
test("rejects a total payload larger than the read byte cap before returning any bytes", async () => {
|
|
1474
|
+
const principal: {
|
|
1475
|
+
request: JSONObject;
|
|
1476
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1477
|
+
} = payloadPrincipal();
|
|
1478
|
+
|
|
1479
|
+
mockProps(principal.databaseProps);
|
|
1480
|
+
mockSessionHeader(applicationAId);
|
|
1481
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1482
|
+
|
|
1483
|
+
chunkQuerySpy.mockResolvedValue(
|
|
1484
|
+
fakeResultSet([
|
|
1485
|
+
{ chunkIndex: 0, chunkStoredBytes: 5 * 1024 * 1024 },
|
|
1486
|
+
{ chunkIndex: 1, chunkStoredBytes: 5 * 1024 * 1024 },
|
|
1487
|
+
]) as never,
|
|
1488
|
+
);
|
|
1489
|
+
|
|
1490
|
+
const result: CallResult = await callRoute({
|
|
1491
|
+
uri: CHUNKS_ROUTE,
|
|
1492
|
+
request: principal.request,
|
|
1493
|
+
body: {
|
|
1494
|
+
sessionId: "session-1",
|
|
1495
|
+
tabId: "tab-1",
|
|
1496
|
+
chunkIndexes: [0, 1],
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1501
|
+
/* Refused on the pre-check: the payload read never ran. */
|
|
1502
|
+
expect(chunkQuerySpy).toHaveBeenCalledTimes(1);
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
/*
|
|
1506
|
+
* The cap has to bound the bytes this endpoint actually returns.
|
|
1507
|
+
* `payloadBytes` is the POST-GZIP wire size the recorder uploaded,
|
|
1508
|
+
* while the `payload` column holds the DECOMPRESSED JSON that is
|
|
1509
|
+
* served. Measuring the former let 8 chunks that pass a 8 MiB check
|
|
1510
|
+
* decompress into tens of megabytes of response.
|
|
1511
|
+
*/
|
|
1512
|
+
test("the pre-check measures the stored (decompressed) size, not the compressed wire size", async () => {
|
|
1513
|
+
const principal: {
|
|
1514
|
+
request: JSONObject;
|
|
1515
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1516
|
+
} = payloadPrincipal();
|
|
1517
|
+
|
|
1518
|
+
mockProps(principal.databaseProps);
|
|
1519
|
+
mockSessionHeader(applicationAId);
|
|
1520
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1521
|
+
|
|
1522
|
+
chunkQuerySpy
|
|
1523
|
+
.mockResolvedValueOnce(
|
|
1524
|
+
fakeResultSet([{ chunkIndex: 0, chunkStoredBytes: 4 }]) as never,
|
|
1525
|
+
)
|
|
1526
|
+
.mockResolvedValueOnce(
|
|
1527
|
+
fakeResultSet([{ chunkIndex: 0, payload: "[1]" }]) as never,
|
|
1528
|
+
);
|
|
1529
|
+
|
|
1530
|
+
await callRoute({
|
|
1531
|
+
uri: CHUNKS_ROUTE,
|
|
1532
|
+
request: principal.request,
|
|
1533
|
+
body: {
|
|
1534
|
+
sessionId: "session-1",
|
|
1535
|
+
tabId: "tab-1",
|
|
1536
|
+
chunkIndexes: [0],
|
|
1537
|
+
},
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
const precheck: Statement = chunkQuerySpy.mock.calls[0]![0] as Statement;
|
|
1541
|
+
|
|
1542
|
+
expect(precheck.query).toContain("length(payload)");
|
|
1543
|
+
expect(precheck.query).not.toContain("toFloat64(payloadBytes)");
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test("refuses to serve more bytes than the cap even when the pre-check said the chunks were small", async () => {
|
|
1547
|
+
const principal: {
|
|
1548
|
+
request: JSONObject;
|
|
1549
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1550
|
+
} = payloadPrincipal();
|
|
1551
|
+
|
|
1552
|
+
mockProps(principal.databaseProps);
|
|
1553
|
+
mockSessionHeader(applicationAId);
|
|
1554
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1555
|
+
|
|
1556
|
+
/* 10 MiB of real payload behind a pre-check that reported 8 bytes. */
|
|
1557
|
+
const fatPayload: string = "a".repeat(5 * 1024 * 1024);
|
|
1558
|
+
|
|
1559
|
+
chunkQuerySpy
|
|
1560
|
+
.mockResolvedValueOnce(
|
|
1561
|
+
fakeResultSet([
|
|
1562
|
+
{ chunkIndex: 0, chunkStoredBytes: 4 },
|
|
1563
|
+
{ chunkIndex: 1, chunkStoredBytes: 4 },
|
|
1564
|
+
]) as never,
|
|
1565
|
+
)
|
|
1566
|
+
.mockResolvedValueOnce(
|
|
1567
|
+
fakeResultSet([
|
|
1568
|
+
{ chunkIndex: 0, payload: fatPayload },
|
|
1569
|
+
{ chunkIndex: 1, payload: fatPayload },
|
|
1570
|
+
]) as never,
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
const result: CallResult = await callRoute({
|
|
1574
|
+
uri: CHUNKS_ROUTE,
|
|
1575
|
+
request: principal.request,
|
|
1576
|
+
body: {
|
|
1577
|
+
sessionId: "session-1",
|
|
1578
|
+
tabId: "tab-1",
|
|
1579
|
+
chunkIndexes: [0, 1],
|
|
1580
|
+
},
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1584
|
+
/* Nothing was served. */
|
|
1585
|
+
expect(result.sentBuffer).toBeUndefined();
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
test("returns length-prefixed binary frames and de-duplicates by version", async () => {
|
|
1589
|
+
const principal: {
|
|
1590
|
+
request: JSONObject;
|
|
1591
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1592
|
+
} = payloadPrincipal();
|
|
1593
|
+
|
|
1594
|
+
mockProps(principal.databaseProps);
|
|
1595
|
+
mockSessionHeader(applicationAId);
|
|
1596
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1597
|
+
|
|
1598
|
+
chunkQuerySpy
|
|
1599
|
+
.mockResolvedValueOnce(
|
|
1600
|
+
fakeResultSet([
|
|
1601
|
+
{ chunkIndex: 0, chunkStoredBytes: 3 },
|
|
1602
|
+
{ chunkIndex: 1, chunkStoredBytes: 4 },
|
|
1603
|
+
]) as never,
|
|
1604
|
+
)
|
|
1605
|
+
.mockResolvedValueOnce(
|
|
1606
|
+
fakeResultSet([
|
|
1607
|
+
{ chunkIndex: 0, payload: "[1]" },
|
|
1608
|
+
{ chunkIndex: 1, payload: "[22]" },
|
|
1609
|
+
]) as never,
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
const result: CallResult = await callRoute({
|
|
1613
|
+
uri: CHUNKS_ROUTE,
|
|
1614
|
+
request: principal.request,
|
|
1615
|
+
body: {
|
|
1616
|
+
sessionId: "session-1",
|
|
1617
|
+
tabId: "tab-1",
|
|
1618
|
+
chunkIndexes: [0, 1],
|
|
1619
|
+
},
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
const buffer: Buffer = result.sentBuffer as unknown as Buffer;
|
|
1623
|
+
expect(buffer).toBeDefined();
|
|
1624
|
+
|
|
1625
|
+
expect(buffer.readUInt32LE(0)).toBe(0);
|
|
1626
|
+
expect(buffer.readUInt32LE(4)).toBe(3);
|
|
1627
|
+
expect(buffer.subarray(8, 11).toString("utf8")).toBe("[1]");
|
|
1628
|
+
expect(buffer.readUInt32LE(11)).toBe(1);
|
|
1629
|
+
expect(buffer.readUInt32LE(15)).toBe(4);
|
|
1630
|
+
expect(buffer.subarray(19, 23).toString("utf8")).toBe("[22]");
|
|
1631
|
+
|
|
1632
|
+
expect(result.headers["Content-Type"]).toBe("application/octet-stream");
|
|
1633
|
+
// A recording is personal data - it must never enter a shared cache.
|
|
1634
|
+
expect(result.headers["Cache-Control"]).toBe("no-store");
|
|
1635
|
+
|
|
1636
|
+
const payloadStatement: Statement = chunkQuerySpy.mock
|
|
1637
|
+
.calls[1]![0] as Statement;
|
|
1638
|
+
expect(payloadStatement.query).toContain(
|
|
1639
|
+
"ORDER BY chunkIndex ASC, version DESC LIMIT 1 BY chunkIndex",
|
|
1640
|
+
);
|
|
1641
|
+
});
|
|
1642
|
+
|
|
1643
|
+
test("rejects a non-integer chunk index", async () => {
|
|
1644
|
+
const principal: {
|
|
1645
|
+
request: JSONObject;
|
|
1646
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1647
|
+
} = payloadPrincipal();
|
|
1648
|
+
|
|
1649
|
+
mockProps(principal.databaseProps);
|
|
1650
|
+
|
|
1651
|
+
const result: CallResult = await callRoute({
|
|
1652
|
+
uri: CHUNKS_ROUTE,
|
|
1653
|
+
request: principal.request,
|
|
1654
|
+
body: {
|
|
1655
|
+
sessionId: "session-1",
|
|
1656
|
+
tabId: "tab-1",
|
|
1657
|
+
chunkIndexes: [0, "1; DROP TABLE"],
|
|
1658
|
+
},
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
expect(result.deniedWith).toBeInstanceOf(BadDataException);
|
|
1662
|
+
expect(chunkQuerySpy).not.toHaveBeenCalled();
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
describe("heartbeat", () => {
|
|
1667
|
+
test("floors seconds watched to the 15s cadence and scopes the row to the tenant", async () => {
|
|
1668
|
+
const principal: {
|
|
1669
|
+
request: JSONObject;
|
|
1670
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1671
|
+
} = buildPrincipal({
|
|
1672
|
+
projectId: projectId,
|
|
1673
|
+
userId: userId,
|
|
1674
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
mockProps(principal.databaseProps);
|
|
1678
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1679
|
+
|
|
1680
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
1681
|
+
mockOwnView({ viewId: viewId, rumApplicationId: applicationAId });
|
|
1682
|
+
|
|
1683
|
+
await callRoute({
|
|
1684
|
+
uri: HEARTBEAT_ROUTE,
|
|
1685
|
+
request: principal.request,
|
|
1686
|
+
body: { viewId: viewId.toString(), secondsWatched: 44 },
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
expect(recordSecondsWatchedSpy).toHaveBeenCalledTimes(1);
|
|
1690
|
+
const args: JSONObject = recordSecondsWatchedSpy.mock
|
|
1691
|
+
.calls[0]![0] as JSONObject;
|
|
1692
|
+
expect(args["secondsWatched"]).toBe(30);
|
|
1693
|
+
expect((args["projectId"] as ObjectID).toString()).toBe(
|
|
1694
|
+
projectId.toString(),
|
|
1695
|
+
);
|
|
1696
|
+
expect((args["viewId"] as ObjectID).toString()).toBe(viewId.toString());
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
test("a Viewer cannot reach the heartbeat route at all", async () => {
|
|
1700
|
+
const principal: {
|
|
1701
|
+
request: JSONObject;
|
|
1702
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1703
|
+
} = buildPrincipal({
|
|
1704
|
+
projectId: projectId,
|
|
1705
|
+
userId: userId,
|
|
1706
|
+
permissions: [Permission.Viewer],
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
mockProps(principal.databaseProps);
|
|
1710
|
+
|
|
1711
|
+
const result: CallResult = await callRoute({
|
|
1712
|
+
uri: HEARTBEAT_ROUTE,
|
|
1713
|
+
request: principal.request,
|
|
1714
|
+
body: { viewId: ObjectID.generate().toString(), secondsWatched: 30 },
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
1718
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
/*
|
|
1722
|
+
* secondsWatched is a privacy control shown on the player, not a
|
|
1723
|
+
* counter. A payload-permitted principal must not be able to inflate
|
|
1724
|
+
* a colleague's audit row - the row is looked up as the caller's own,
|
|
1725
|
+
* so somebody else's viewId matches nothing.
|
|
1726
|
+
*/
|
|
1727
|
+
test("a viewId belonging to another user is refused and never written", async () => {
|
|
1728
|
+
const principal: {
|
|
1729
|
+
request: JSONObject;
|
|
1730
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1731
|
+
} = buildPrincipal({
|
|
1732
|
+
projectId: projectId,
|
|
1733
|
+
userId: userId,
|
|
1734
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
mockProps(principal.databaseProps);
|
|
1738
|
+
mockApplication({ id: applicationAId, labelIds: [] });
|
|
1739
|
+
|
|
1740
|
+
const someoneElsesViewId: ObjectID = ObjectID.generate();
|
|
1741
|
+
|
|
1742
|
+
/*
|
|
1743
|
+
* The service is asked for a row owned by the caller; the row in
|
|
1744
|
+
* the table belongs to somebody else, so nothing comes back.
|
|
1745
|
+
*/
|
|
1746
|
+
viewFindOneBySpy.mockResolvedValue(null);
|
|
1747
|
+
|
|
1748
|
+
const result: CallResult = await callRoute({
|
|
1749
|
+
uri: HEARTBEAT_ROUTE,
|
|
1750
|
+
request: principal.request,
|
|
1751
|
+
body: {
|
|
1752
|
+
viewId: someoneElsesViewId.toString(),
|
|
1753
|
+
secondsWatched: 600,
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
expect(result.deniedWith).toBeInstanceOf(NotAuthorizedException);
|
|
1758
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
1759
|
+
|
|
1760
|
+
/* The ownership predicate is the thing being pinned. */
|
|
1761
|
+
const lookupArgs: JSONObject = viewFindOneBySpy.mock
|
|
1762
|
+
.calls[0]![0] as JSONObject;
|
|
1763
|
+
const query: JSONObject = lookupArgs["query"] as JSONObject;
|
|
1764
|
+
expect((query["viewedByUserId"] as ObjectID).toString()).toBe(
|
|
1765
|
+
userId.toString(),
|
|
1766
|
+
);
|
|
1767
|
+
expect((query["projectId"] as ObjectID).toString()).toBe(
|
|
1768
|
+
projectId.toString(),
|
|
1769
|
+
);
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
test("a caller outside the view's application label scope cannot advance it", async () => {
|
|
1773
|
+
const principal: {
|
|
1774
|
+
request: JSONObject;
|
|
1775
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1776
|
+
} = buildPrincipal({
|
|
1777
|
+
projectId: projectId,
|
|
1778
|
+
userId: userId,
|
|
1779
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1780
|
+
labelIds: [labelAId],
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
mockProps(principal.databaseProps);
|
|
1784
|
+
/* The view points at application B, which carries a foreign label. */
|
|
1785
|
+
mockApplication({ id: applicationBId, labelIds: [labelBId] });
|
|
1786
|
+
|
|
1787
|
+
const viewId: ObjectID = ObjectID.generate();
|
|
1788
|
+
mockOwnView({ viewId: viewId, rumApplicationId: applicationBId });
|
|
1789
|
+
|
|
1790
|
+
const result: CallResult = await callRoute({
|
|
1791
|
+
uri: HEARTBEAT_ROUTE,
|
|
1792
|
+
request: principal.request,
|
|
1793
|
+
body: { viewId: viewId.toString(), secondsWatched: 30 },
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
1797
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
1798
|
+
});
|
|
1799
|
+
|
|
1800
|
+
test("a malformed viewId is a bad request, not a database error", async () => {
|
|
1801
|
+
const principal: {
|
|
1802
|
+
request: JSONObject;
|
|
1803
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1804
|
+
} = buildPrincipal({
|
|
1805
|
+
projectId: projectId,
|
|
1806
|
+
userId: userId,
|
|
1807
|
+
permissions: [Permission.ReadRumSessionReplayPayload],
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
mockProps(principal.databaseProps);
|
|
1811
|
+
|
|
1812
|
+
const result: CallResult = await callRoute({
|
|
1813
|
+
uri: HEARTBEAT_ROUTE,
|
|
1814
|
+
request: principal.request,
|
|
1815
|
+
body: { viewId: "nope", secondsWatched: 30 },
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
expect(result.thrownToNext).toBeInstanceOf(BadDataException);
|
|
1819
|
+
expect(viewFindOneBySpy).not.toHaveBeenCalled();
|
|
1820
|
+
expect(recordSecondsWatchedSpy).not.toHaveBeenCalled();
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
describe("for-exception", () => {
|
|
1825
|
+
test("filters to the applications a label-scoped caller can reach", async () => {
|
|
1826
|
+
const principal: {
|
|
1827
|
+
request: JSONObject;
|
|
1828
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1829
|
+
} = buildPrincipal({
|
|
1830
|
+
projectId: projectId,
|
|
1831
|
+
userId: userId,
|
|
1832
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
1833
|
+
labelIds: [labelAId],
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
mockProps(principal.databaseProps);
|
|
1837
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
1838
|
+
|
|
1839
|
+
await callRoute({
|
|
1840
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
1841
|
+
request: principal.request,
|
|
1842
|
+
body: { fingerprint: "fp-1" },
|
|
1843
|
+
});
|
|
1844
|
+
|
|
1845
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1846
|
+
.calls[0]![0] as Statement;
|
|
1847
|
+
|
|
1848
|
+
expect(statement.query).toContain("hasAny(exceptionFingerprints,");
|
|
1849
|
+
expect(statement.query).toContain("rumApplicationId IN (");
|
|
1850
|
+
expect(Object.values(statement.query_params)).toContainEqual([
|
|
1851
|
+
applicationAId.toString(),
|
|
1852
|
+
]);
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
test("a label-scoped caller that reaches no application gets no rows and issues no query", async () => {
|
|
1856
|
+
const principal: {
|
|
1857
|
+
request: JSONObject;
|
|
1858
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1859
|
+
} = buildPrincipal({
|
|
1860
|
+
projectId: projectId,
|
|
1861
|
+
userId: userId,
|
|
1862
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
1863
|
+
labelIds: [labelAId],
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
mockProps(principal.databaseProps);
|
|
1867
|
+
findBySpy.mockResolvedValue([]);
|
|
1868
|
+
|
|
1869
|
+
const result: CallResult = await callRoute({
|
|
1870
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
1871
|
+
request: principal.request,
|
|
1872
|
+
body: { fingerprint: "fp-1" },
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
expect(headerQuerySpy).not.toHaveBeenCalled();
|
|
1876
|
+
expect(result.jsonBody?.["sessions"]).toEqual([]);
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
test("an unscoped caller queries the whole project without an application filter", async () => {
|
|
1880
|
+
const principal: {
|
|
1881
|
+
request: JSONObject;
|
|
1882
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1883
|
+
} = buildPrincipal({
|
|
1884
|
+
projectId: projectId,
|
|
1885
|
+
userId: userId,
|
|
1886
|
+
permissions: [Permission.ProjectOwner],
|
|
1887
|
+
});
|
|
1888
|
+
|
|
1889
|
+
mockProps(principal.databaseProps);
|
|
1890
|
+
|
|
1891
|
+
await callRoute({
|
|
1892
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
1893
|
+
request: principal.request,
|
|
1894
|
+
body: { fingerprint: "fp-1" },
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1898
|
+
.calls[0]![0] as Statement;
|
|
1899
|
+
|
|
1900
|
+
expect(statement.query).not.toContain("rumApplicationId IN (");
|
|
1901
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
test("projects the frustration counters and masking mode the replay card renders", async () => {
|
|
1905
|
+
const principal: {
|
|
1906
|
+
request: JSONObject;
|
|
1907
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
1908
|
+
} = buildPrincipal({
|
|
1909
|
+
projectId: projectId,
|
|
1910
|
+
userId: userId,
|
|
1911
|
+
permissions: [Permission.ProjectOwner],
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
mockProps(principal.databaseProps);
|
|
1915
|
+
|
|
1916
|
+
headerQuerySpy.mockResolvedValue(
|
|
1917
|
+
fakeResultSet([
|
|
1918
|
+
{
|
|
1919
|
+
sessionId: "session-9",
|
|
1920
|
+
applicationId: applicationAId.toString(),
|
|
1921
|
+
aggStartTime: "2026-07-30 10:00:00.000",
|
|
1922
|
+
aggEndTime: "2026-07-30 10:01:00.000",
|
|
1923
|
+
aggDurationMs: 60000,
|
|
1924
|
+
aggHasError: 1,
|
|
1925
|
+
aggErrorCount: 2,
|
|
1926
|
+
aggRageClickCount: 3,
|
|
1927
|
+
aggDeadClickCount: 4,
|
|
1928
|
+
aggErrorClickCount: 6,
|
|
1929
|
+
aggRefreshRageCount: 5,
|
|
1930
|
+
aggMaskingMode: "MaskAllText",
|
|
1931
|
+
aggTriggerReason: "error",
|
|
1932
|
+
aggEntryUrl: "https://shop.example.com/checkout",
|
|
1933
|
+
aggBrowserName: "Chrome",
|
|
1934
|
+
aggOsName: "macOS",
|
|
1935
|
+
aggDeviceType: "desktop",
|
|
1936
|
+
aggIsFinalized: 1,
|
|
1937
|
+
},
|
|
1938
|
+
]) as never,
|
|
1939
|
+
);
|
|
1940
|
+
|
|
1941
|
+
const result: CallResult = await callRoute({
|
|
1942
|
+
uri: FOR_EXCEPTION_ROUTE,
|
|
1943
|
+
request: principal.request,
|
|
1944
|
+
body: { fingerprint: "fp-1" },
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
/*
|
|
1948
|
+
* The card's signals line ("3 rage clicks before the error") and its
|
|
1949
|
+
* up-front masking disclosure are fed exclusively by these aliases.
|
|
1950
|
+
* The client parses a missing key to 0/"", so dropping one from the
|
|
1951
|
+
* SELECT silently degrades the card — it must fail here instead.
|
|
1952
|
+
*/
|
|
1953
|
+
const statement: Statement = headerQuerySpy.mock
|
|
1954
|
+
.calls[0]![0] as Statement;
|
|
1955
|
+
|
|
1956
|
+
for (const alias of [
|
|
1957
|
+
"aggRageClickCount",
|
|
1958
|
+
"aggDeadClickCount",
|
|
1959
|
+
"aggErrorClickCount",
|
|
1960
|
+
"aggRefreshRageCount",
|
|
1961
|
+
"aggMaskingMode",
|
|
1962
|
+
]) {
|
|
1963
|
+
expect(statement.query).toContain(alias);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const sessions: Array<JSONObject> = result.jsonBody?.[
|
|
1967
|
+
"sessions"
|
|
1968
|
+
] as unknown as Array<JSONObject>;
|
|
1969
|
+
|
|
1970
|
+
expect(sessions).toHaveLength(1);
|
|
1971
|
+
expect(sessions[0]!["rumApplicationId"]).toBe(applicationAId.toString());
|
|
1972
|
+
expect(sessions[0]!["rageClickCount"]).toBe(3);
|
|
1973
|
+
expect(sessions[0]!["deadClickCount"]).toBe(4);
|
|
1974
|
+
expect(sessions[0]!["errorClickCount"]).toBe(6);
|
|
1975
|
+
expect(sessions[0]!["refreshRageCount"]).toBe(5);
|
|
1976
|
+
expect(sessions[0]!["maskingMode"]).toBe("MaskAllText");
|
|
1977
|
+
});
|
|
1978
|
+
});
|
|
1979
|
+
|
|
1980
|
+
describe("ingest-status", () => {
|
|
1981
|
+
function mockConfiguredApplication(): void {
|
|
1982
|
+
const application: RumApplication = new RumApplication();
|
|
1983
|
+
application.id = applicationAId;
|
|
1984
|
+
application.projectId = projectId;
|
|
1985
|
+
application.labels = [];
|
|
1986
|
+
application.appIdentifier = "checkout-web";
|
|
1987
|
+
application.isSessionReplayEnabled = true;
|
|
1988
|
+
application.sessionReplayAllowedOrigins = ["https://shop.example.com"];
|
|
1989
|
+
application.sessionReplaySamplePercentage = 25;
|
|
1990
|
+
application.sessionReplayMonthlyBudgetInGB = 2;
|
|
1991
|
+
|
|
1992
|
+
findOneBySpy.mockResolvedValue(application);
|
|
1993
|
+
findBySpy.mockResolvedValue([application]);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function mockProject(isAllowed: boolean): void {
|
|
1997
|
+
const project: Project = new Project();
|
|
1998
|
+
project.id = projectId;
|
|
1999
|
+
project.isSessionReplayAllowed = isAllowed;
|
|
2000
|
+
|
|
2001
|
+
jest.spyOn(ProjectService, "findOneBy").mockResolvedValue(project);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
test("a label-scoped caller whose labels do not reach the application is refused before any disclosure", async () => {
|
|
2005
|
+
const principal: {
|
|
2006
|
+
request: JSONObject;
|
|
2007
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2008
|
+
} = buildPrincipal({
|
|
2009
|
+
projectId: projectId,
|
|
2010
|
+
userId: userId,
|
|
2011
|
+
permissions: [Permission.ReadRumSessionReplay],
|
|
2012
|
+
labelIds: [labelBId],
|
|
2013
|
+
});
|
|
2014
|
+
|
|
2015
|
+
mockProps(principal.databaseProps);
|
|
2016
|
+
mockApplication({ id: applicationAId, labelIds: [labelAId] });
|
|
2017
|
+
mockProject(true);
|
|
2018
|
+
|
|
2019
|
+
const result: CallResult = await callRoute({
|
|
2020
|
+
uri: INGEST_STATUS_ROUTE,
|
|
2021
|
+
request: principal.request,
|
|
2022
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
2026
|
+
expect(result.jsonBody).toBeUndefined();
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
test("an application outside this tenant is refused identically to a missing one", async () => {
|
|
2030
|
+
const principal: {
|
|
2031
|
+
request: JSONObject;
|
|
2032
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2033
|
+
} = buildPrincipal({
|
|
2034
|
+
projectId: projectId,
|
|
2035
|
+
userId: userId,
|
|
2036
|
+
permissions: [Permission.ProjectOwner],
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
mockProps(principal.databaseProps);
|
|
2040
|
+
findOneBySpy.mockResolvedValue(null);
|
|
2041
|
+
mockProject(true);
|
|
2042
|
+
|
|
2043
|
+
const result: CallResult = await callRoute({
|
|
2044
|
+
uri: INGEST_STATUS_ROUTE,
|
|
2045
|
+
request: principal.request,
|
|
2046
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
expect(result.thrownToNext).toBeInstanceOf(NotAuthorizedException);
|
|
2050
|
+
expect(result.jsonBody).toBeUndefined();
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
test("returns the switches, health timestamps and budget figures the panel renders", async () => {
|
|
2054
|
+
const principal: {
|
|
2055
|
+
request: JSONObject;
|
|
2056
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
2057
|
+
} = buildPrincipal({
|
|
2058
|
+
projectId: projectId,
|
|
2059
|
+
userId: userId,
|
|
2060
|
+
permissions: [Permission.ProjectOwner],
|
|
2061
|
+
});
|
|
2062
|
+
|
|
2063
|
+
mockProps(principal.databaseProps);
|
|
2064
|
+
mockConfiguredApplication();
|
|
2065
|
+
mockProject(true);
|
|
2066
|
+
|
|
2067
|
+
const result: CallResult = await callRoute({
|
|
2068
|
+
uri: INGEST_STATUS_ROUTE,
|
|
2069
|
+
request: principal.request,
|
|
2070
|
+
body: { rumApplicationId: applicationAId.toString() },
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
const body: JSONObject = result.jsonBody as JSONObject;
|
|
2074
|
+
|
|
2075
|
+
expect(body["isProjectAllowed"]).toBe(true);
|
|
2076
|
+
expect(body["isApplicationEnabled"]).toBe(true);
|
|
2077
|
+
expect(body["appIdentifier"]).toBe("checkout-web");
|
|
2078
|
+
expect(body["allowedOrigins"]).toEqual(["https://shop.example.com"]);
|
|
2079
|
+
expect(body["samplePercentage"]).toBe(25);
|
|
2080
|
+
expect(body["monthlyBudgetInGB"]).toBe(2);
|
|
2081
|
+
expect(body["lastChunkReceivedAt"]).toBeNull();
|
|
2082
|
+
expect(body["budgetExceededAt"]).toBeNull();
|
|
2083
|
+
/*
|
|
2084
|
+
* Jest runs without Redis, so the usage readers answer null —
|
|
2085
|
+
* "unknown", which the panel must render as unknown, never as 0.
|
|
2086
|
+
* The daily limit is config, not a counter, so it is always known.
|
|
2087
|
+
*/
|
|
2088
|
+
expect(body["projectBytesUsedToday"]).toBeNull();
|
|
2089
|
+
expect(body["applicationBytesUsedThisMonth"]).toBeNull();
|
|
2090
|
+
expect(body["dailyByteLimit"]).toBeGreaterThan(0);
|
|
2091
|
+
});
|
|
2092
|
+
});
|
|
2093
|
+
});
|