@oneuptime/common 12.0.33 → 12.0.34
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/RumSession.ts +117 -1
- package/Models/AnalyticsModels/RumSessionChunk.ts +13 -0
- package/Models/DatabaseModels/AlertCustomField.ts +77 -3
- package/Models/DatabaseModels/ApiKeyPermission.ts +3 -27
- package/Models/DatabaseModels/IncidentCustomField.ts +77 -3
- package/Models/DatabaseModels/InventoryItemCustomField.ts +77 -3
- package/Models/DatabaseModels/MonitorCustomField.ts +77 -3
- package/Models/DatabaseModels/OnCallDutyPolicyCustomField.ts +77 -3
- package/Models/DatabaseModels/RumApplication.ts +6 -6
- package/Models/DatabaseModels/ScheduledMaintenanceCustomField.ts +77 -3
- package/Models/DatabaseModels/StatusPageCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamMember.ts +0 -9
- package/Models/DatabaseModels/TeamMemberCustomField.ts +77 -3
- package/Models/DatabaseModels/TeamPermission.ts +3 -24
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +260 -0
- package/Models/DatabaseModels/UserTelegram.ts +0 -4
- package/Scripts/benchmark-fanin-capacity.js +181 -0
- package/Server/API/TelemetryAPI.ts +1284 -164
- package/Server/API/UserNotificationSettingAPI.ts +55 -0
- package/Server/API/UserTelegramAPI.ts +48 -4
- package/Server/EnvironmentConfig.ts +35 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.ts +67 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.ts +139 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.ts +121 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/QueueWorker.ts +20 -14
- package/Server/Infrastructure/Semaphore.ts +2 -0
- package/Server/Middleware/TelemetryIngest.ts +453 -7
- package/Server/Services/AccessTokenService.ts +1 -0
- package/Server/Services/AlertCustomFieldService.ts +98 -0
- package/Server/Services/AlertService.ts +37 -0
- package/Server/Services/ApiKeyPermissionService.ts +461 -29
- package/Server/Services/CustomFieldMappingService.ts +879 -0
- package/Server/Services/GlobalConfigService.ts +194 -0
- package/Server/Services/IncidentCustomFieldService.ts +98 -0
- package/Server/Services/IncidentService.ts +35 -0
- package/Server/Services/MetricService.ts +194 -0
- package/Server/Services/MonitorService.ts +32 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +78 -25
- package/Server/Services/ProjectService.ts +22 -0
- package/Server/Services/RoutineEmailSettingsService.ts +73 -0
- package/Server/Services/RumSessionReplayViewService.ts +104 -24
- package/Server/Services/ScheduledMaintenanceCustomFieldService.ts +98 -0
- package/Server/Services/ScheduledMaintenanceService.ts +32 -0
- package/Server/Services/TeamMemberService.ts +78 -0
- package/Server/Services/TeamPermissionService.ts +235 -3
- package/Server/Services/TelemetryIngestionKeyService.ts +722 -19
- package/Server/Services/UserNotificationSettingService.ts +34 -6
- package/Server/Services/UserTelegramService.ts +326 -5
- package/Server/Types/Database/QueryHelper.ts +2 -0
- package/Server/Utils/APIKey/AccessPermission.ts +5 -2
- package/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.ts +54 -0
- package/Server/Utils/CustomField/CustomFieldMappingRegistry.ts +415 -0
- package/Server/Utils/CustomField/CustomFieldMappingValidator.ts +335 -0
- package/Server/Utils/DataSource/EgressGuard.ts +177 -8
- package/Server/Utils/EmailRollup/EmailRollupConstants.ts +38 -5
- package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +57 -4
- package/Server/Utils/FrontendEnvironment.ts +40 -0
- package/Server/Utils/LogRedaction.ts +20 -0
- package/Server/Utils/Logger.ts +11 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +338 -77
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +30 -4
- package/Server/Utils/Monitor/MonitorAlert.ts +29 -6
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +110 -15
- package/Server/Utils/Monitor/MonitorIncident.ts +22 -6
- package/Server/Utils/Monitor/MonitorTemplateUtil.ts +29 -0
- package/Server/Utils/Monitor/SeriesContextEnricher.ts +304 -0
- package/Server/Utils/SSRFProtection.ts +217 -10
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +239 -138
- package/Server/Utils/SessionReplay/SessionReplayHealthCounters.ts +305 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +1430 -289
- package/Server/Utils/StartServer.ts +2 -17
- package/Server/Utils/TelegramVerificationToken.ts +185 -0
- package/Server/Utils/Telemetry/PinServiceName.ts +197 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +38 -16
- package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +92 -0
- package/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.ts +217 -0
- package/Server/Utils/VM/VMAPI.ts +1 -0
- package/Server/Utils/VM/VMRunner.ts +975 -95
- package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +202 -0
- package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +404 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +468 -0
- package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +399 -0
- package/Tests/App/StatusPage/StatusPageLastUpdated.test.tsx +263 -0
- package/Tests/App/StatusPage/StatusPageOverviewLiveAndSearch.test.tsx +753 -0
- package/Tests/App/StatusPage/StatusPageResourceSearchBox.test.tsx +224 -0
- package/Tests/Models/AnalyticsModels/RumSessionReplayColumns.test.ts +262 -0
- package/Tests/Models/CustomFieldMappingColumns.test.ts +166 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +20 -6
- package/Tests/ResponsiveVisibility.test.ts +115 -0
- package/Tests/ResponsiveVisibility.ts +183 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +1940 -216
- package/Tests/Server/API/UserNotificationSettingAPI.test.ts +186 -0
- package/Tests/Server/API/UserTelegramAPISecurity.test.ts +231 -0
- package/Tests/Server/EnvironmentConfigFrontendSecurity.test.ts +234 -0
- package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +202 -0
- package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +41 -0
- package/Tests/Server/Infrastructure/Postgres/SessionReplayRecordEverySessionByDefaultMigration.test.ts +169 -0
- package/Tests/Server/Infrastructure/QueueWorkerTimeout.test.ts +210 -0
- package/Tests/Server/Infrastructure/TelemetryExporterDeploymentConfig.test.ts +130 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -1
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +1163 -0
- package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +5 -3
- package/Tests/Server/Services/AddTelemetryIngestionKeyTypeMigration.test.ts +555 -0
- package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +678 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +276 -28
- package/Tests/Server/Services/CustomFieldDropdownOptionsColumnWidth.test.ts +431 -0
- package/Tests/Server/Services/CustomFieldMappingService.test.ts +850 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +32 -0
- package/Tests/Server/Services/GlobalConfigService.test.ts +414 -1
- package/Tests/Server/Services/MetricRawEntityKeyPrune.test.ts +592 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +226 -1
- package/Tests/Server/Services/RoutineEmailSettingsPostgres.test.ts +329 -0
- package/Tests/Server/Services/RumSessionReplayViewService.test.ts +267 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +9 -0
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +9 -0
- package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +834 -0
- package/Tests/Server/Services/TelemetryIngestionKeyPolicyResolution.test.ts +814 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +998 -0
- package/Tests/Server/Services/UserNotificationSettingRollupRouting.test.ts +141 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +18 -10
- package/Tests/Server/Services/UserTelegramVerificationSecurity.test.ts +688 -0
- package/Tests/Server/Utils/AI/SRE/Insights/FixRouting.test.ts +497 -0
- package/Tests/Server/Utils/AI/Toolbox/Serializer.test.ts +383 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +7 -3
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterConfig.test.ts +315 -0
- package/Tests/Server/Utils/CustomField/CustomFieldMappingValidator.test.ts +437 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +300 -11
- package/Tests/Server/Utils/EmailRollup/EmailRollupBurstWindow.test.ts +83 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
- package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerPreferences.test.ts +362 -0
- package/Tests/Server/Utils/EmailRollup/EmailRollupTestHarness.ts +77 -1
- package/Tests/Server/Utils/FrontendEnvironment.test.ts +190 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +26 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +23 -9
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +584 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestHeaderCriteria.test.ts +236 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +437 -0
- package/Tests/Server/Utils/Monitor/MonitorTemplateUtilSeriesContext.test.ts +176 -0
- package/Tests/Server/Utils/Monitor/SeriesContextEnricher.test.ts +356 -0
- package/Tests/Server/Utils/SSRFProtectionCloudServiceAddresses.test.ts +595 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +198 -1
- package/Tests/Server/Utils/SessionReplay/SessionReplayHealthCounters.test.ts +453 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1378 -0
- package/Tests/Server/Utils/SessionReplayOriginAllowListRefactor.test.ts +420 -0
- package/Tests/Server/Utils/TelegramVerificationToken.test.ts +253 -0
- package/Tests/Server/Utils/Telemetry/PinServiceName.test.ts +666 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriterCapacity.test.ts +458 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.test.ts +424 -0
- package/Tests/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.test.ts +528 -0
- package/Tests/Server/Utils/TelemetryExporterEnvironment.test.ts +56 -0
- package/Tests/Server/Utils/VM/VMRunnerHostBridgeLatency.test.ts +74 -0
- package/Tests/Server/Utils/VM/VMRunnerPrivateNetworkWiring.test.ts +100 -7
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +1176 -6
- package/Tests/Types/CustomField/CustomFieldMappingCatalog.test.ts +220 -0
- package/Tests/Types/CustomField/CustomFieldValueMapping.test.ts +348 -0
- package/Tests/Types/JSONFunctions.test.ts +260 -0
- package/Tests/Types/Monitor/CephAlertTemplates.test.ts +422 -53
- package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +495 -15
- package/Tests/Types/Monitor/DockerSwarmAlertTemplates.test.ts +191 -45
- package/Tests/Types/Monitor/HostAlertTemplates.test.ts +370 -42
- package/Tests/Types/Monitor/IotAlertTemplates.test.ts +375 -20
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +636 -42
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +189 -0
- package/Tests/Types/Monitor/KubernetesTemplateGroupByKeys.test.ts +95 -14
- package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +190 -21
- package/Tests/Types/Monitor/ProxmoxAlertTemplates.test.ts +352 -22
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationAlertDebuggability.test.ts +363 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +93 -18
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +259 -0
- package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +476 -0
- package/Tests/Types/Monitor/RumAlertTemplates.test.ts +136 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesDebugHints.test.ts +661 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +507 -0
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +270 -0
- package/Tests/Types/Monitor/TemplateGroupByKeys.test.ts +38 -10
- package/Tests/Types/Monitor/Utils/RecommendationCriteriaAssertions.ts +102 -0
- package/Tests/Types/NotificationSetting/RoutineEmailEvents.test.ts +47 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +608 -0
- package/Tests/Types/Rum/SessionReplayCustomEvents.test.ts +434 -0
- package/Tests/Types/WebsiteRequest.test.ts +285 -3
- package/Tests/UI/Components/Charts/ChartBucketIdentity.test.ts +16 -3
- package/Tests/UI/Components/Charts/ChartTrailingBucketGap.test.ts +691 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +12 -4
- package/Tests/UI/Components/ComponentsModalUsability.test.tsx +518 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetailMapping.test.tsx +324 -0
- package/Tests/UI/Components/CustomFields/MapFromCustomFieldInput.test.tsx +176 -0
- package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +484 -0
- package/Tests/UI/Components/HeaderRightRail.test.tsx +157 -0
- package/Tests/UI/Components/IconDropdownItem.test.tsx +102 -0
- package/Tests/UI/Components/JSONTablePrototypePollution.test.tsx +117 -0
- package/Tests/UI/Components/KeyboardShortcutsModal.test.tsx +214 -0
- package/Tests/UI/Components/ModelTable/ModelTableWrapContent.test.tsx +561 -0
- package/Tests/UI/Components/MonitorGraphs/UptimeBarDayModal.test.tsx +302 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +28 -12
- package/Tests/UI/Components/ShortcutDialogGuard.test.tsx +74 -0
- package/Tests/UI/Components/StatusPage/ResourceGroupSectionAutoExpand.test.tsx +248 -0
- package/Tests/UI/Components/TableCellWrapping.test.tsx +625 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +205 -0
- package/Tests/UI/Components/Workflow/NodePlacement.test.ts +148 -0
- package/Tests/UI/Components/Workflow/Workflow.test.tsx +981 -0
- package/Tests/UI/ConfigBrowserTelemetry.test.ts +70 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +1036 -2
- package/Tests/UI/Rum/FidelityNotices.test.ts +187 -0
- package/Tests/UI/Rum/InactivityMap.test.ts +341 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +259 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +855 -0
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +594 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +451 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +536 -0
- package/Tests/UI/Rum/ReplayEngine.test.ts +2503 -0
- package/Tests/UI/Rum/ReplayEngineTypes.test.ts +171 -0
- package/Tests/UI/Rum/ReplayHeader.test.tsx +711 -0
- package/Tests/UI/Rum/ReplayLink.test.tsx +119 -0
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +408 -0
- package/Tests/UI/Rum/ReplayPlaybackIntent.test.ts +152 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +1296 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +900 -0
- package/Tests/UI/Rum/ReplayScrubber.test.tsx +763 -147
- package/Tests/UI/Rum/ReplaySignalTypes.test.ts +189 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +1579 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +396 -473
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +918 -0
- package/Tests/UI/Rum/ReplayTimeline.test.tsx +696 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +499 -0
- package/Tests/UI/Rum/SessionReplaySearchBar.test.tsx +346 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +545 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +991 -0
- package/Tests/UI/Rum/TargetedCapturePanel.test.tsx +254 -0
- package/Tests/UI/Telemetry/BrowserExporterIsolation.test.ts +71 -0
- package/Tests/UI/Utils/GlobalKeyboardShortcut.test.ts +344 -0
- package/Tests/Utils/API.test.ts +308 -0
- package/Tests/Utils/HTTPResponseBodyReader.test.ts +323 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveryScanStatus.test.ts +117 -0
- package/Tests/Utils/Rum/ChunkMath.test.ts +83 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +993 -0
- package/Tests/Utils/Rum/SessionReplayStringMap.test.ts +245 -0
- package/Tests/Utils/StatusPage/ResourceSearch.test.ts +631 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +633 -0
- package/Tests/Utils/Uptime/DayUptimeGraphUtil.test.ts +461 -0
- package/Tests/Utils/ValueFormatter.test.ts +148 -0
- package/Types/CustomField/CustomFieldMappingCatalog.ts +161 -0
- package/Types/CustomField/CustomFieldMappingSourceResource.ts +19 -0
- package/Types/CustomField/CustomFieldValueMapping.ts +288 -0
- package/Types/Icon/IconProp.ts +1 -0
- package/Types/JSONFunctions.ts +189 -43
- package/Types/Monitor/CephAlertTemplates.ts +157 -195
- package/Types/Monitor/DockerAlertTemplates.ts +338 -128
- package/Types/Monitor/DockerSwarmAlertTemplates.ts +95 -112
- package/Types/Monitor/HostAlertTemplates.ts +302 -130
- package/Types/Monitor/IotAlertTemplates.ts +112 -104
- package/Types/Monitor/KubernetesAlertTemplates.ts +415 -224
- package/Types/Monitor/KubernetesMetricCatalog.ts +23 -19
- package/Types/Monitor/PodmanAlertTemplates.ts +116 -163
- package/Types/Monitor/ProxmoxAlertTemplates.ts +217 -99
- package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +98 -6
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +44 -7
- package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +383 -0
- package/Types/Monitor/RumAlertTemplates.ts +46 -7
- package/Types/Monitor/SeriesContext/SeriesDebugHints.ts +596 -0
- package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +554 -0
- package/Types/Monitor/ServiceAlertTemplates.ts +98 -11
- package/Types/Monitor/UptimeHistoryLabels.ts +56 -0
- package/Types/NotificationSetting/RoutineEmailEvents.ts +30 -0
- package/Types/Rum/SessionReplay.ts +209 -0
- package/Types/Rum/SessionReplayApi.ts +722 -0
- package/Types/Rum/SessionReplayCaptureTrigger.ts +21 -12
- package/Types/Rum/SessionReplayConsentMode.ts +11 -7
- package/Types/Rum/SessionReplayCustomEvents.ts +549 -0
- package/Types/Rum/SessionReplayHealth.ts +238 -0
- package/Types/Telemetry/TelemetryIngestSurface.ts +95 -0
- package/Types/Telemetry/TelemetryIngestionKeyPolicy.ts +75 -0
- package/Types/Telemetry/TelemetryIngestionKeyType.ts +29 -0
- package/Types/WebsiteRequest.ts +85 -3
- package/UI/Components/Charts/Area/AreaChart.tsx +13 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -2
- package/UI/Components/Charts/Line/LineChart.tsx +13 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +29 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +22 -3
- package/UI/Components/Charts/Utils/TimeAnnotation.ts +76 -9
- package/UI/Components/Charts/Utils/XAxis.ts +217 -0
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +143 -25
- package/UI/Components/CustomFields/MapFromCustomFieldInput.tsx +216 -0
- package/UI/Components/Graphs/DayUptimeGraph.tsx +173 -8
- package/UI/Components/Graphs/UptimeBarTooltip.tsx +22 -278
- package/UI/Components/Graphs/UptimeDaySummary.tsx +327 -0
- package/UI/Components/Header/Header.tsx +25 -9
- package/UI/Components/Header/IconDropdown/IconDropdownItem.tsx +10 -1
- package/UI/Components/Header/IconDropdown/IconDropdownMenu.tsx +6 -1
- package/UI/Components/Header/ProjectPicker/ProjectPicker.tsx +6 -1
- package/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.tsx +6 -1
- package/UI/Components/Icon/Icon.tsx +11 -0
- package/UI/Components/JSONTable/JSONTable.tsx +2 -2
- package/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.tsx +125 -0
- package/UI/Components/KeyboardShortcut/Screenshots/README.md +19 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-command-palette.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog-dark.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-dialog.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-help-menu.png +0 -0
- package/UI/Components/KeyboardShortcut/Screenshots/keyboard-shortcuts-narrow.png +0 -0
- package/UI/Components/ModelTable/Column.ts +15 -0
- package/UI/Components/Monitor/SeriesDebugCommandsViewer.tsx +64 -0
- package/UI/Components/Monitor/SeriesLabelsViewer.tsx +93 -0
- package/UI/Components/MonitorGraphs/Uptime.tsx +14 -2
- package/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +63 -10
- package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +44 -0
- package/UI/Components/StatusPage/ResourceGroupSection.tsx +40 -0
- package/UI/Components/Table/CellClassName.ts +81 -0
- package/UI/Components/Table/TableRow.tsx +20 -14
- package/UI/Components/Table/TableSkeletonRows.tsx +9 -9
- package/UI/Components/Table/Types/Column.ts +36 -0
- package/UI/Components/Tooltip/Tooltip.tsx +11 -1
- package/UI/Components/Workflow/ComponentsModal.tsx +60 -39
- package/UI/Components/Workflow/NodePlacement.ts +57 -0
- package/UI/Components/Workflow/Workflow.tsx +47 -18
- package/UI/Config.ts +15 -33
- package/UI/Utils/GlobalKeyboardShortcut.ts +208 -0
- package/UI/Utils/Telemetry/BrowserTelemetryConfig.ts +28 -0
- package/UI/Utils/Telemetry/Telemetry.ts +10 -8
- package/Utils/API.ts +129 -3
- package/Utils/HTTPResponseBodyReader.ts +221 -0
- package/Utils/NetworkDiscovery/DiscoveryScanStatus.ts +77 -0
- package/Utils/Rum/ChunkMath.ts +106 -0
- package/Utils/Rum/SessionReplayHealth.ts +732 -0
- package/Utils/Rum/SessionReplayStringMap.ts +226 -0
- package/Utils/Schema/ModelSchema.ts +1 -0
- package/Utils/StatusPage/ResourceSearch.ts +290 -0
- package/Utils/Telemetry/OriginAllowList.ts +355 -0
- package/Utils/Uptime/DayUptimeGraphUtil.ts +205 -0
- package/Utils/ValueFormatter.ts +35 -1
- package/build/dist/Models/AnalyticsModels/RumSession.js +102 -19
- package/build/dist/Models/AnalyticsModels/RumSession.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +11 -9
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/AlertCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ApiKeyPermission.js +3 -27
- package/build/dist/Models/DatabaseModels/ApiKeyPermission.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/IncidentCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/MonitorCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +6 -6
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/StatusPageCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/TeamCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMember.js +0 -9
- package/build/dist/Models/DatabaseModels/TeamMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js +79 -3
- package/build/dist/Models/DatabaseModels/TeamMemberCustomField.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TeamPermission.js +3 -24
- package/build/dist/Models/DatabaseModels/TeamPermission.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +267 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js +0 -4
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +769 -109
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationSettingAPI.js +35 -0
- package/build/dist/Server/API/UserNotificationSettingAPI.js.map +1 -0
- package/build/dist/Server/API/UserTelegramAPI.js +28 -6
- package/build/dist/Server/API/UserTelegramAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +29 -2
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js +60 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791300000000-AddTelemetryIngestionKeyType.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js +53 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791400000000-SessionReplayRecordEverySessionByDefault.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js +82 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791500000000-WidenCustomFieldDropdownOptions.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791600000000-AddCustomFieldValueMapping.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +19 -7
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Infrastructure/Semaphore.js +1 -0
- package/build/dist/Server/Infrastructure/Semaphore.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +315 -8
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AccessTokenService.js +1 -0
- package/build/dist/Server/Services/AccessTokenService.js.map +1 -1
- package/build/dist/Server/Services/AlertCustomFieldService.js +93 -0
- package/build/dist/Server/Services/AlertCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +34 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +331 -28
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/CustomFieldMappingService.js +614 -0
- package/build/dist/Server/Services/CustomFieldMappingService.js.map +1 -0
- package/build/dist/Server/Services/GlobalConfigService.js +133 -0
- package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
- package/build/dist/Server/Services/IncidentCustomFieldService.js +93 -0
- package/build/dist/Server/Services/IncidentCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +32 -0
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +163 -0
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +29 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +79 -27
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +22 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/RoutineEmailSettingsService.js +69 -0
- package/build/dist/Server/Services/RoutineEmailSettingsService.js.map +1 -0
- package/build/dist/Server/Services/RumSessionReplayViewService.js +86 -24
- package/build/dist/Server/Services/RumSessionReplayViewService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js +93 -0
- package/build/dist/Server/Services/ScheduledMaintenanceCustomFieldService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +29 -0
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +62 -2
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamPermissionService.js +166 -3
- package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js +562 -18
- package/build/dist/Server/Services/TelemetryIngestionKeyService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -6
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserTelegramService.js +257 -4
- package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +2 -2
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js +27 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldDefinitionMappingHooks.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js +226 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingRegistry.js.map +1 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js +189 -0
- package/build/dist/Server/Utils/CustomField/CustomFieldMappingValidator.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +121 -7
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js +38 -5
- package/build/dist/Server/Utils/EmailRollup/EmailRollupConstants.js.map +1 -1
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +44 -2
- package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
- package/build/dist/Server/Utils/FrontendEnvironment.js +33 -0
- package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -0
- package/build/dist/Server/Utils/LogRedaction.js +20 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -1
- package/build/dist/Server/Utils/Logger.js +10 -0
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +287 -68
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +20 -4
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +29 -6
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +88 -13
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +22 -0
- package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js +237 -0
- package/build/dist/Server/Utils/Monitor/SeriesContextEnricher.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +136 -9
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +174 -89
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js +223 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayHealthCounters.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +945 -164
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +3 -14
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TelegramVerificationToken.js +129 -0
- package/build/dist/Server/Utils/TelegramVerificationToken.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/PinServiceName.js +163 -0
- package/build/dist/Server/Utils/Telemetry/PinServiceName.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +28 -15
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +64 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js +171 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyRateLimiter.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +780 -69
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js +60 -0
- package/build/dist/Types/CustomField/CustomFieldMappingCatalog.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js +20 -0
- package/build/dist/Types/CustomField/CustomFieldMappingSourceResource.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldValueMapping.js +141 -0
- package/build/dist/Types/CustomField/CustomFieldValueMapping.js.map +1 -0
- package/build/dist/Types/Icon/IconProp.js +1 -0
- package/build/dist/Types/Icon/IconProp.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +118 -37
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Monitor/CephAlertTemplates.js +130 -171
- package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/DockerAlertTemplates.js +286 -121
- package/build/dist/Types/Monitor/DockerAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js +84 -105
- package/build/dist/Types/Monitor/DockerSwarmAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/HostAlertTemplates.js +250 -122
- package/build/dist/Types/Monitor/HostAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/IotAlertTemplates.js +81 -81
- package/build/dist/Types/Monitor/IotAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +375 -215
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +19 -19
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/PodmanAlertTemplates.js +104 -153
- package/build/dist/Types/Monitor/PodmanAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +156 -91
- package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +67 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +38 -7
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +261 -0
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -0
- package/build/dist/Types/Monitor/RumAlertTemplates.js +46 -7
- package/build/dist/Types/Monitor/RumAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js +422 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesDebugHints.js.map +1 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +441 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +79 -15
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/UptimeHistoryLabels.js +17 -0
- package/build/dist/Types/Monitor/UptimeHistoryLabels.js.map +1 -0
- package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js +29 -0
- package/build/dist/Types/NotificationSetting/RoutineEmailEvents.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplay.js +117 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js +181 -0
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js +21 -12
- package/build/dist/Types/Rum/SessionReplayCaptureTrigger.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayConsentMode.js +11 -7
- package/build/dist/Types/Rum/SessionReplayConsentMode.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js +172 -0
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplayHealth.js +37 -0
- package/build/dist/Types/Rum/SessionReplayHealth.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js +85 -0
- package/build/dist/Types/Telemetry/TelemetryIngestSurface.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js +15 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyPolicy.js.map +1 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js +30 -0
- package/build/dist/Types/Telemetry/TelemetryIngestionKeyType.js.map +1 -0
- package/build/dist/Types/WebsiteRequest.js +55 -3
- package/build/dist/Types/WebsiteRequest.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +6 -2
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +13 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Types/XAxis/XAxis.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +15 -3
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js +40 -9
- package/build/dist/UI/Components/Charts/Utils/TimeAnnotation.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/XAxis.js +186 -0
- package/build/dist/UI/Components/Charts/Utils/XAxis.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +80 -6
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js +118 -0
- package/build/dist/UI/Components/CustomFields/MapFromCustomFieldInput.js.map +1 -0
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +112 -7
- package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js +5 -162
- package/build/dist/UI/Components/Graphs/UptimeBarTooltip.js.map +1 -1
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js +181 -0
- package/build/dist/UI/Components/Graphs/UptimeDaySummary.js.map +1 -0
- package/build/dist/UI/Components/Header/Header.js +2 -3
- package/build/dist/UI/Components/Header/Header.js.map +1 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js +2 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownItem.js.map +1 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js +7 -1
- package/build/dist/UI/Components/Header/IconDropdown/IconDropdownMenu.js.map +1 -1
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js +7 -1
- package/build/dist/UI/Components/Header/ProjectPicker/ProjectPicker.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js +7 -1
- package/build/dist/UI/Components/HeaderAlert/NotificationBell/NotificationBellDropdown.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +5 -0
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/JSONTable/JSONTable.js +2 -2
- package/build/dist/UI/Components/JSONTable/JSONTable.js.map +1 -1
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js +35 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcutsModal.js.map +1 -0
- package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js +34 -0
- package/build/dist/UI/Components/Monitor/SeriesDebugCommandsViewer.js.map +1 -0
- package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js +49 -0
- package/build/dist/UI/Components/Monitor/SeriesLabelsViewer.js.map +1 -0
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js +1 -1
- package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js +32 -6
- package/build/dist/UI/Components/MonitorGraphs/UptimeBarDayModal.js.map +1 -1
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +38 -0
- package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js +22 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupSection.js.map +1 -1
- package/build/dist/UI/Components/Table/CellClassName.js +55 -0
- package/build/dist/UI/Components/Table/CellClassName.js.map +1 -0
- package/build/dist/UI/Components/Table/TableRow.js +16 -13
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +9 -8
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js +25 -1
- package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +47 -30
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/NodePlacement.js +37 -0
- package/build/dist/UI/Components/Workflow/NodePlacement.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Workflow.js +35 -18
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Config.js +10 -20
- package/build/dist/UI/Config.js.map +1 -1
- package/build/dist/UI/Utils/GlobalKeyboardShortcut.js +138 -0
- package/build/dist/UI/Utils/GlobalKeyboardShortcut.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js +14 -0
- package/build/dist/UI/Utils/Telemetry/BrowserTelemetryConfig.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/Telemetry.js +7 -5
- package/build/dist/UI/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Utils/API.js +67 -5
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/HTTPResponseBodyReader.js +157 -0
- package/build/dist/Utils/HTTPResponseBodyReader.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js +69 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryScanStatus.js.map +1 -0
- package/build/dist/Utils/Rum/ChunkMath.js +76 -0
- package/build/dist/Utils/Rum/ChunkMath.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +534 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -0
- package/build/dist/Utils/Rum/SessionReplayStringMap.js +165 -0
- package/build/dist/Utils/Rum/SessionReplayStringMap.js.map +1 -0
- package/build/dist/Utils/Schema/ModelSchema.js +1 -0
- package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
- package/build/dist/Utils/StatusPage/ResourceSearch.js +200 -0
- package/build/dist/Utils/StatusPage/ResourceSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +299 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js +147 -0
- package/build/dist/Utils/Uptime/DayUptimeGraphUtil.js.map +1 -0
- package/build/dist/Utils/ValueFormatter.js +32 -1
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +1 -1
|
@@ -121,24 +121,48 @@ import SessionReplayIdentity from "../Utils/SessionReplay/SessionReplayIdentity"
|
|
|
121
121
|
import SessionReplayTargeting from "../Utils/SessionReplay/SessionReplayTargeting";
|
|
122
122
|
import SessionReplayUsage from "../Utils/SessionReplay/SessionReplayUsage";
|
|
123
123
|
import RumSessionReplayView from "../../Models/DatabaseModels/RumSessionReplayView";
|
|
124
|
-
import RumSessionReplayViewService
|
|
124
|
+
import RumSessionReplayViewService, {
|
|
125
|
+
normalizeSecondsWatched,
|
|
126
|
+
} from "../Services/RumSessionReplayViewService";
|
|
125
127
|
import SessionReplayReadService, {
|
|
126
128
|
DEFAULT_SESSION_REPLAY_LIST_LIMIT,
|
|
127
129
|
MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT,
|
|
128
|
-
|
|
130
|
+
MAX_SESSION_REPLAY_LIST_LIMIT,
|
|
131
|
+
SESSION_REPLAY_EXCEPTION_WINDOW_PADDING_MS,
|
|
132
|
+
SessionReplayApplicationActivitySummary,
|
|
133
|
+
SessionReplayChunkReadResult,
|
|
129
134
|
SessionReplayExceptionSession,
|
|
135
|
+
SessionReplayExpiredSessionInfo,
|
|
130
136
|
SessionReplayListCursor,
|
|
131
137
|
SessionReplayListFilters,
|
|
132
138
|
SessionReplayListResult,
|
|
133
139
|
SessionReplayManifest,
|
|
134
140
|
SessionReplaySessionHeader,
|
|
141
|
+
SessionReplaySessionIdentity,
|
|
135
142
|
} from "../Utils/SessionReplay/SessionReplayReadService";
|
|
143
|
+
import SessionReplayHealthCounters, {
|
|
144
|
+
SessionReplayDropCount,
|
|
145
|
+
} from "../Utils/SessionReplay/SessionReplayHealthCounters";
|
|
146
|
+
import { isSessionErased } from "../Utils/SessionReplay/SessionReplayErasureTombstone";
|
|
147
|
+
import NotFoundException from "../../Types/Exception/NotFoundException";
|
|
136
148
|
import {
|
|
137
149
|
DEFAULT_SESSION_REPLAY_MAX_BYTES_PER_PROJECT_PER_DAY,
|
|
138
150
|
MAX_SESSION_REPLAY_CHUNKS_PER_READ,
|
|
139
151
|
MAX_SESSION_REPLAY_READ_BYTES,
|
|
152
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH,
|
|
153
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS,
|
|
154
|
+
SESSION_REPLAY_MAX_SESSION_MS,
|
|
155
|
+
SESSION_REPLAY_MAX_TAG_KEYS,
|
|
156
|
+
SESSION_REPLAY_MAX_TAG_KEY_LENGTH,
|
|
157
|
+
SESSION_REPLAY_MAX_TAG_VALUE_LENGTH,
|
|
140
158
|
SESSION_REPLAY_MAX_USER_REF_LENGTH,
|
|
141
159
|
} from "../../Types/Rum/SessionReplay";
|
|
160
|
+
import {
|
|
161
|
+
SESSION_REPLAY_SORT_BY_VALUES,
|
|
162
|
+
SessionReplaySortBy,
|
|
163
|
+
parseSessionReplayListCursor,
|
|
164
|
+
} from "../../Types/Rum/SessionReplayApi";
|
|
165
|
+
import { SessionReplayRefusalCount } from "../../Types/Rum/SessionReplayHealth";
|
|
142
166
|
|
|
143
167
|
const router: ExpressRouter = Express.getRouter();
|
|
144
168
|
|
|
@@ -3834,24 +3858,92 @@ const isApplicationInSessionReplayScope: IsApplicationInSessionReplayScopeFuncti
|
|
|
3834
3858
|
* Returns the application so callers that need its labels for a second,
|
|
3835
3859
|
* narrower decision (the identity column) do not have to load it twice.
|
|
3836
3860
|
*/
|
|
3837
|
-
|
|
3861
|
+
/*
|
|
3862
|
+
* Process-local memory of what the two playback hot paths re-derive on
|
|
3863
|
+
* every call: which application a session belongs to (a ClickHouse GROUP
|
|
3864
|
+
* BY over the header table) and which labels that application carries (a
|
|
3865
|
+
* Postgres lookup). A 480-chunk session is 60 chunk pages, and a live
|
|
3866
|
+
* player heartbeats every 15s; without this every one of those paid both
|
|
3867
|
+
* lookups again for an answer that had not changed.
|
|
3868
|
+
*
|
|
3869
|
+
* What is cached is the DATA, never the decision: the label intersection
|
|
3870
|
+
* is recomputed against the caller's current permissions on every
|
|
3871
|
+
* request, so a revoked grant takes effect immediately. A label edit on
|
|
3872
|
+
* the application, or a header re-resolution, lags by at most the TTL -
|
|
3873
|
+
* the same lag the ingest gate's policy cache already accepts. The
|
|
3874
|
+
* manifest route always resolves fresh (a live session's header changes)
|
|
3875
|
+
* and refills the entries the chunk and heartbeat routes then read.
|
|
3876
|
+
*/
|
|
3877
|
+
const SESSION_REPLAY_AUTHORIZATION_CACHE_TTL_MS: number = 30 * 1000;
|
|
3878
|
+
const MAX_SESSION_REPLAY_AUTHORIZATION_CACHE_ENTRIES: number = 2000;
|
|
3879
|
+
|
|
3880
|
+
interface CachedRumApplication {
|
|
3881
|
+
application: RumApplication;
|
|
3882
|
+
expiresAt: number;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
interface CachedSessionHeader {
|
|
3886
|
+
header: SessionReplaySessionHeader;
|
|
3887
|
+
expiresAt: number;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
const rumApplicationCache: Map<string, CachedRumApplication> = new Map<
|
|
3891
|
+
string,
|
|
3892
|
+
CachedRumApplication
|
|
3893
|
+
>();
|
|
3894
|
+
const sessionHeaderCache: Map<string, CachedSessionHeader> = new Map<
|
|
3895
|
+
string,
|
|
3896
|
+
CachedSessionHeader
|
|
3897
|
+
>();
|
|
3898
|
+
|
|
3899
|
+
type BoundedCacheSetFunction = <TValue>(
|
|
3900
|
+
map: Map<string, TValue>,
|
|
3901
|
+
key: string,
|
|
3902
|
+
value: TValue,
|
|
3903
|
+
) => void;
|
|
3904
|
+
|
|
3905
|
+
const boundedCacheSet: BoundedCacheSetFunction = <TValue>(
|
|
3906
|
+
map: Map<string, TValue>,
|
|
3907
|
+
key: string,
|
|
3908
|
+
value: TValue,
|
|
3909
|
+
): void => {
|
|
3910
|
+
if (
|
|
3911
|
+
map.size >= MAX_SESSION_REPLAY_AUTHORIZATION_CACHE_ENTRIES &&
|
|
3912
|
+
!map.has(key)
|
|
3913
|
+
) {
|
|
3914
|
+
const oldest: string | undefined = map.keys().next().value;
|
|
3915
|
+
|
|
3916
|
+
if (oldest !== undefined) {
|
|
3917
|
+
map.delete(oldest);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
map.delete(key);
|
|
3922
|
+
map.set(key, value);
|
|
3923
|
+
};
|
|
3924
|
+
|
|
3925
|
+
type LoadRumApplicationForAccessFunction = (data: {
|
|
3838
3926
|
projectId: ObjectID;
|
|
3839
3927
|
rumApplicationId: ObjectID;
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
}) => Promise<RumApplication>;
|
|
3928
|
+
allowCached: boolean;
|
|
3929
|
+
}) => Promise<RumApplication | null>;
|
|
3843
3930
|
|
|
3844
|
-
const
|
|
3931
|
+
const loadRumApplicationForAccess: LoadRumApplicationForAccessFunction =
|
|
3845
3932
|
async (data: {
|
|
3846
3933
|
projectId: ObjectID;
|
|
3847
3934
|
rumApplicationId: ObjectID;
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3935
|
+
allowCached: boolean;
|
|
3936
|
+
}): Promise<RumApplication | null> => {
|
|
3937
|
+
const cacheKey: string = `${data.projectId.toString()}:${data.rumApplicationId.toString()}`;
|
|
3938
|
+
|
|
3939
|
+
if (data.allowCached) {
|
|
3940
|
+
const cached: CachedRumApplication | undefined =
|
|
3941
|
+
rumApplicationCache.get(cacheKey);
|
|
3942
|
+
|
|
3943
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3944
|
+
return cached.application;
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3855
3947
|
|
|
3856
3948
|
const application: RumApplication | null =
|
|
3857
3949
|
await RumApplicationService.findOneBy({
|
|
@@ -3870,6 +3962,45 @@ const assertSessionReplayApplicationAccess: AssertSessionReplayApplicationAccess
|
|
|
3870
3962
|
},
|
|
3871
3963
|
});
|
|
3872
3964
|
|
|
3965
|
+
if (application) {
|
|
3966
|
+
boundedCacheSet(rumApplicationCache, cacheKey, {
|
|
3967
|
+
application: application,
|
|
3968
|
+
expiresAt: Date.now() + SESSION_REPLAY_AUTHORIZATION_CACHE_TTL_MS,
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
return application;
|
|
3973
|
+
};
|
|
3974
|
+
|
|
3975
|
+
type AssertSessionReplayApplicationAccessFunction = (data: {
|
|
3976
|
+
projectId: ObjectID;
|
|
3977
|
+
rumApplicationId: ObjectID;
|
|
3978
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3979
|
+
permissions: Array<Permission>;
|
|
3980
|
+
/* Serve the application's labels from the short-lived cache. */
|
|
3981
|
+
allowCached?: boolean | undefined;
|
|
3982
|
+
}) => Promise<RumApplication>;
|
|
3983
|
+
|
|
3984
|
+
const assertSessionReplayApplicationAccess: AssertSessionReplayApplicationAccessFunction =
|
|
3985
|
+
async (data: {
|
|
3986
|
+
projectId: ObjectID;
|
|
3987
|
+
rumApplicationId: ObjectID;
|
|
3988
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
3989
|
+
permissions: Array<Permission>;
|
|
3990
|
+
allowCached?: boolean | undefined;
|
|
3991
|
+
}): Promise<RumApplication> => {
|
|
3992
|
+
const scope: SessionReplayScope = getSessionReplayLabelScope(
|
|
3993
|
+
data.databaseProps,
|
|
3994
|
+
data.permissions,
|
|
3995
|
+
);
|
|
3996
|
+
|
|
3997
|
+
const application: RumApplication | null =
|
|
3998
|
+
await loadRumApplicationForAccess({
|
|
3999
|
+
projectId: data.projectId,
|
|
4000
|
+
rumApplicationId: data.rumApplicationId,
|
|
4001
|
+
allowCached: data.allowCached === true,
|
|
4002
|
+
});
|
|
4003
|
+
|
|
3873
4004
|
/*
|
|
3874
4005
|
* An application that does not exist in this project is refused the
|
|
3875
4006
|
* same way an out-of-scope one is, so the endpoint cannot be used to
|
|
@@ -4032,6 +4163,179 @@ const resolveAccessibleRumApplicationIds: ResolveAccessibleRumApplicationIdsFunc
|
|
|
4032
4163
|
return { applicationIds: accessibleIds, isTruncated: isTruncated };
|
|
4033
4164
|
};
|
|
4034
4165
|
|
|
4166
|
+
interface AuthorizedSession {
|
|
4167
|
+
header: SessionReplaySessionHeader;
|
|
4168
|
+
/*
|
|
4169
|
+
* The RumApplication the header was authorized against, with its
|
|
4170
|
+
* labels, so a caller that needs a second, narrower decision (the
|
|
4171
|
+
* identity columns) does not load it again.
|
|
4172
|
+
*/
|
|
4173
|
+
application: RumApplication;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
/*
|
|
4177
|
+
* Why a sessionId has no playable header. Each answer carries a
|
|
4178
|
+
* different code word in its message so a client can tell them apart
|
|
4179
|
+
* without parsing prose, and each is a 404: the id was well-formed, there
|
|
4180
|
+
* is simply nothing at it.
|
|
4181
|
+
*/
|
|
4182
|
+
const GENERIC_MISSING_SESSION_MESSAGE: string =
|
|
4183
|
+
"not-found: No session replay exists with this id in this project.";
|
|
4184
|
+
|
|
4185
|
+
/*
|
|
4186
|
+
* May this caller be told anything specific about a recording belonging to
|
|
4187
|
+
* the named application?
|
|
4188
|
+
*
|
|
4189
|
+
* Deliberately NOT assertSessionReplayApplicationAccess: this runs on the
|
|
4190
|
+
* path where the header row is already gone, so the application row may
|
|
4191
|
+
* legitimately be gone with it, and an unrestricted caller (a project
|
|
4192
|
+
* owner, or anyone holding an unscoped payload grant) must not lose the
|
|
4193
|
+
* "expired on <date>" answer because of a deleted application. Only a
|
|
4194
|
+
* label-scoped caller needs the row, and for them a row that cannot be
|
|
4195
|
+
* loaded is refused.
|
|
4196
|
+
*/
|
|
4197
|
+
type IsApplicationInSessionReplayScopeByIdFunction = (data: {
|
|
4198
|
+
projectId: ObjectID;
|
|
4199
|
+
rumApplicationId: string;
|
|
4200
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
4201
|
+
}) => Promise<boolean>;
|
|
4202
|
+
|
|
4203
|
+
const isApplicationInSessionReplayScopeById: IsApplicationInSessionReplayScopeByIdFunction =
|
|
4204
|
+
async (data: {
|
|
4205
|
+
projectId: ObjectID;
|
|
4206
|
+
rumApplicationId: string;
|
|
4207
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
4208
|
+
}): Promise<boolean> => {
|
|
4209
|
+
let scope: SessionReplayScope;
|
|
4210
|
+
|
|
4211
|
+
try {
|
|
4212
|
+
scope = getSessionReplayLabelScope(
|
|
4213
|
+
data.databaseProps,
|
|
4214
|
+
SESSION_REPLAY_PAYLOAD_PERMISSIONS,
|
|
4215
|
+
);
|
|
4216
|
+
} catch {
|
|
4217
|
+
/* A scope this path cannot enforce is refused, never widened. */
|
|
4218
|
+
return false;
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
if (scope.isUnrestricted) {
|
|
4222
|
+
return true;
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
if (!ObjectID.isValidUUID(data.rumApplicationId)) {
|
|
4226
|
+
return false;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
const application: RumApplication | null =
|
|
4230
|
+
await loadRumApplicationForAccess({
|
|
4231
|
+
projectId: data.projectId,
|
|
4232
|
+
rumApplicationId: new ObjectID(data.rumApplicationId),
|
|
4233
|
+
allowCached: true,
|
|
4234
|
+
});
|
|
4235
|
+
|
|
4236
|
+
if (!application) {
|
|
4237
|
+
return false;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
return isApplicationInSessionReplayScope({
|
|
4241
|
+
scope: scope,
|
|
4242
|
+
application: application,
|
|
4243
|
+
});
|
|
4244
|
+
};
|
|
4245
|
+
|
|
4246
|
+
type ExplainMissingSessionFunction = (data: {
|
|
4247
|
+
projectId: ObjectID;
|
|
4248
|
+
sessionId: string;
|
|
4249
|
+
rumApplicationId: ObjectID | undefined;
|
|
4250
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
4251
|
+
}) => Promise<NotFoundException>;
|
|
4252
|
+
|
|
4253
|
+
const explainMissingSession: ExplainMissingSessionFunction = async (data: {
|
|
4254
|
+
projectId: ObjectID;
|
|
4255
|
+
sessionId: string;
|
|
4256
|
+
rumApplicationId: ObjectID | undefined;
|
|
4257
|
+
databaseProps: DatabaseCommonInteractionProps;
|
|
4258
|
+
}): Promise<NotFoundException> => {
|
|
4259
|
+
/*
|
|
4260
|
+
* The expired header is resolved FIRST, and not because it is the more
|
|
4261
|
+
* likely answer: it is the only lookup that names an application, and
|
|
4262
|
+
* nothing specific may be disclosed until that application has been
|
|
4263
|
+
* authorized.
|
|
4264
|
+
*
|
|
4265
|
+
* The expiry answer carries the recording's existence, when it started
|
|
4266
|
+
* and what retention the owning application runs. Answered before the
|
|
4267
|
+
* access check - which is where it used to sit, since this runs on the
|
|
4268
|
+
* path where getSessionHeader found nothing to authorize AGAINST - it let
|
|
4269
|
+
* a label-scoped reviewer probe session ids and learn all three for
|
|
4270
|
+
* applications outside their scope. That is precisely the existence
|
|
4271
|
+
* probing assertSessionReplayApplicationAccess refuses "the same way" for
|
|
4272
|
+
* a session that DOES exist.
|
|
4273
|
+
*
|
|
4274
|
+
* This read is deliberately retention-free (an expired row is the whole
|
|
4275
|
+
* point), so it is the one place a scope check has to be written out by
|
|
4276
|
+
* hand rather than inherited from resolveAuthorizedSession.
|
|
4277
|
+
*/
|
|
4278
|
+
const expired: SessionReplayExpiredSessionInfo | null =
|
|
4279
|
+
await SessionReplayReadService.getExpiredSessionInfo({
|
|
4280
|
+
projectId: data.projectId,
|
|
4281
|
+
sessionId: data.sessionId,
|
|
4282
|
+
rumApplicationId: data.rumApplicationId,
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
if (expired && expired.rumApplicationId) {
|
|
4286
|
+
if (
|
|
4287
|
+
!(await isApplicationInSessionReplayScopeById({
|
|
4288
|
+
projectId: data.projectId,
|
|
4289
|
+
rumApplicationId: expired.rumApplicationId,
|
|
4290
|
+
databaseProps: data.databaseProps,
|
|
4291
|
+
}))
|
|
4292
|
+
) {
|
|
4293
|
+
return new NotFoundException(GENERIC_MISSING_SESSION_MESSAGE);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
|
|
4297
|
+
/*
|
|
4298
|
+
* Erasure is checked only once the caller is entitled to a specific
|
|
4299
|
+
* answer. An erased session may still have a header row until the
|
|
4300
|
+
* ClickHouse mutation lands, and "expired" would be the wrong story for
|
|
4301
|
+
* a recording that was deliberately destroyed - so it is reported ahead
|
|
4302
|
+
* of expiry. The tombstone throws when Redis cannot answer; that is a
|
|
4303
|
+
* "cannot tell", which falls through to the header-based answers.
|
|
4304
|
+
*/
|
|
4305
|
+
let isErased: boolean = false;
|
|
4306
|
+
|
|
4307
|
+
try {
|
|
4308
|
+
isErased = await isSessionErased({
|
|
4309
|
+
projectId: data.projectId.toString(),
|
|
4310
|
+
sessionId: data.sessionId,
|
|
4311
|
+
});
|
|
4312
|
+
} catch {
|
|
4313
|
+
isErased = false;
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
if (isErased) {
|
|
4317
|
+
return new NotFoundException(
|
|
4318
|
+
"erased: This recording was erased by a data subject request and cannot be played back.",
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
if (expired) {
|
|
4323
|
+
const retentionDays: number = Math.max(
|
|
4324
|
+
1,
|
|
4325
|
+
Math.round(
|
|
4326
|
+
(expired.expiresAt.getTime() - expired.startTime.getTime()) /
|
|
4327
|
+
(24 * 60 * 60 * 1000),
|
|
4328
|
+
),
|
|
4329
|
+
);
|
|
4330
|
+
|
|
4331
|
+
return new NotFoundException(
|
|
4332
|
+
`expired: This recording expired on ${expired.expiresAt.toISOString()} under the application's ${retentionDays}-day retention. Its session signals may still be available from logs, traces and exceptions.`,
|
|
4333
|
+
);
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
return new NotFoundException(GENERIC_MISSING_SESSION_MESSAGE);
|
|
4337
|
+
};
|
|
4338
|
+
|
|
4035
4339
|
/*
|
|
4036
4340
|
* Resolve a caller-supplied sessionId to its header, scoped strictly to
|
|
4037
4341
|
* the tenant, and authorize the owning application. Every payload-bearing
|
|
@@ -4042,34 +4346,172 @@ type ResolveAuthorizedSessionFunction = (data: {
|
|
|
4042
4346
|
projectId: ObjectID;
|
|
4043
4347
|
sessionId: string;
|
|
4044
4348
|
databaseProps: DatabaseCommonInteractionProps;
|
|
4045
|
-
|
|
4349
|
+
/*
|
|
4350
|
+
* Disambiguator only (see getSessionHeader): which application's
|
|
4351
|
+
* recording to read when the same sessionId exists under several. The
|
|
4352
|
+
* application the resolved header names is what gets authorized, so a
|
|
4353
|
+
* caller cannot widen access by naming one.
|
|
4354
|
+
*/
|
|
4355
|
+
rumApplicationId?: ObjectID | undefined;
|
|
4356
|
+
/*
|
|
4357
|
+
* Serve the header and the application's labels from the short-lived
|
|
4358
|
+
* caches. For the chunk and heartbeat hot paths; the manifest resolves
|
|
4359
|
+
* fresh so a live session's header is never stale for a poll.
|
|
4360
|
+
*/
|
|
4361
|
+
allowCached?: boolean | undefined;
|
|
4362
|
+
}) => Promise<AuthorizedSession>;
|
|
4046
4363
|
|
|
4047
4364
|
const resolveAuthorizedSession: ResolveAuthorizedSessionFunction =
|
|
4048
4365
|
async (data: {
|
|
4049
4366
|
projectId: ObjectID;
|
|
4050
4367
|
sessionId: string;
|
|
4051
4368
|
databaseProps: DatabaseCommonInteractionProps;
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4369
|
+
rumApplicationId?: ObjectID | undefined;
|
|
4370
|
+
allowCached?: boolean | undefined;
|
|
4371
|
+
}): Promise<AuthorizedSession> => {
|
|
4372
|
+
const headerCacheKey: string = `${data.projectId.toString()}:${data.sessionId}:${
|
|
4373
|
+
data.rumApplicationId ? data.rumApplicationId.toString() : ""
|
|
4374
|
+
}`;
|
|
4375
|
+
|
|
4376
|
+
let header: SessionReplaySessionHeader | null = null;
|
|
4377
|
+
|
|
4378
|
+
if (data.allowCached) {
|
|
4379
|
+
const cached: CachedSessionHeader | undefined =
|
|
4380
|
+
sessionHeaderCache.get(headerCacheKey);
|
|
4381
|
+
|
|
4382
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
4383
|
+
header = cached.header;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
if (!header) {
|
|
4388
|
+
header = await SessionReplayReadService.getSessionHeader({
|
|
4055
4389
|
projectId: data.projectId,
|
|
4056
4390
|
sessionId: data.sessionId,
|
|
4391
|
+
rumApplicationId: data.rumApplicationId,
|
|
4057
4392
|
});
|
|
4058
4393
|
|
|
4394
|
+
if (header) {
|
|
4395
|
+
boundedCacheSet(sessionHeaderCache, headerCacheKey, {
|
|
4396
|
+
header: header,
|
|
4397
|
+
expiresAt: Date.now() + SESSION_REPLAY_AUTHORIZATION_CACHE_TTL_MS,
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4059
4402
|
if (!header) {
|
|
4060
|
-
throw
|
|
4403
|
+
throw await explainMissingSession({
|
|
4404
|
+
projectId: data.projectId,
|
|
4405
|
+
sessionId: data.sessionId,
|
|
4406
|
+
rumApplicationId: data.rumApplicationId,
|
|
4407
|
+
databaseProps: data.databaseProps,
|
|
4408
|
+
});
|
|
4061
4409
|
}
|
|
4062
4410
|
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4411
|
+
const application: RumApplication =
|
|
4412
|
+
await assertSessionReplayApplicationAccess({
|
|
4413
|
+
projectId: data.projectId,
|
|
4414
|
+
rumApplicationId: new ObjectID(header.rumApplicationId),
|
|
4415
|
+
databaseProps: data.databaseProps,
|
|
4416
|
+
permissions: SESSION_REPLAY_PAYLOAD_PERMISSIONS,
|
|
4417
|
+
allowCached: data.allowCached,
|
|
4418
|
+
});
|
|
4069
4419
|
|
|
4070
|
-
return header;
|
|
4420
|
+
return { header: header, application: application };
|
|
4071
4421
|
};
|
|
4072
4422
|
|
|
4423
|
+
/*
|
|
4424
|
+
* An optional application id off the body: absent or empty means "not
|
|
4425
|
+
* given", anything else must be a well-formed id.
|
|
4426
|
+
*/
|
|
4427
|
+
type ReadOptionalObjectIdFromBodyFunction = (
|
|
4428
|
+
body: JSONObject,
|
|
4429
|
+
key: string,
|
|
4430
|
+
) => ObjectID | undefined;
|
|
4431
|
+
|
|
4432
|
+
const readOptionalObjectIdFromBody: ReadOptionalObjectIdFromBodyFunction = (
|
|
4433
|
+
body: JSONObject,
|
|
4434
|
+
key: string,
|
|
4435
|
+
): ObjectID | undefined => {
|
|
4436
|
+
const value: unknown = body[key];
|
|
4437
|
+
|
|
4438
|
+
if (value === undefined || value === null || value === "") {
|
|
4439
|
+
return undefined;
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
if (typeof value !== "string" || !ObjectID.isValidUUID(value)) {
|
|
4443
|
+
throw new BadDataException(`${key} is not a valid id`);
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
return new ObjectID(value);
|
|
4447
|
+
};
|
|
4448
|
+
|
|
4449
|
+
/*
|
|
4450
|
+
* Optional ISO timestamps off the body. A value that is present but does
|
|
4451
|
+
* not parse is a bad request, not an Invalid Date bound into ClickHouse
|
|
4452
|
+
* (which surfaces as a driver error and a 500).
|
|
4453
|
+
*/
|
|
4454
|
+
type ReadOptionalDateFromBodyFunction = (
|
|
4455
|
+
body: JSONObject,
|
|
4456
|
+
key: string,
|
|
4457
|
+
) => Date | undefined;
|
|
4458
|
+
|
|
4459
|
+
const readOptionalDateFromBody: ReadOptionalDateFromBodyFunction = (
|
|
4460
|
+
body: JSONObject,
|
|
4461
|
+
key: string,
|
|
4462
|
+
): Date | undefined => {
|
|
4463
|
+
const value: unknown = body[key];
|
|
4464
|
+
|
|
4465
|
+
if (value === undefined || value === null || value === "") {
|
|
4466
|
+
return undefined;
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
4470
|
+
throw new BadDataException(`${key} must be an ISO-8601 timestamp`);
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
const parsed: Date =
|
|
4474
|
+
typeof value === "number"
|
|
4475
|
+
? new Date(value)
|
|
4476
|
+
: OneUptimeDate.fromString(value);
|
|
4477
|
+
|
|
4478
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
4479
|
+
throw new BadDataException(`${key} must be an ISO-8601 timestamp`);
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
return parsed;
|
|
4483
|
+
};
|
|
4484
|
+
|
|
4485
|
+
/*
|
|
4486
|
+
* A page size off the body: absent means the default; present means a
|
|
4487
|
+
* positive integer, because `LIMIT 2.5` is a driver error and a 500.
|
|
4488
|
+
*/
|
|
4489
|
+
type ReadLimitFromBodyFunction = (
|
|
4490
|
+
body: JSONObject,
|
|
4491
|
+
defaultLimit: number,
|
|
4492
|
+
maxLimit: number,
|
|
4493
|
+
) => number;
|
|
4494
|
+
|
|
4495
|
+
const readLimitFromBody: ReadLimitFromBodyFunction = (
|
|
4496
|
+
body: JSONObject,
|
|
4497
|
+
defaultLimit: number,
|
|
4498
|
+
maxLimit: number,
|
|
4499
|
+
): number => {
|
|
4500
|
+
const value: unknown = body["limit"];
|
|
4501
|
+
|
|
4502
|
+
if (value === undefined || value === null) {
|
|
4503
|
+
return defaultLimit;
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
4507
|
+
throw new BadDataException(
|
|
4508
|
+
`limit must be a whole number between 1 and ${maxLimit}`,
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
return Math.min(value, maxLimit);
|
|
4513
|
+
};
|
|
4514
|
+
|
|
4073
4515
|
/*
|
|
4074
4516
|
* Object ids arrive as untrusted strings. They are always bound as query
|
|
4075
4517
|
* parameters rather than interpolated, so a bad value cannot inject SQL -
|
|
@@ -4104,6 +4546,14 @@ const readObjectIdFromBody: ReadObjectIdFromBodyFunction = (
|
|
|
4104
4546
|
|
|
4105
4547
|
type ReadSessionIdFromBodyFunction = (body: JSONObject) => string;
|
|
4106
4548
|
|
|
4549
|
+
/*
|
|
4550
|
+
* A session id is 32 hex characters minted in the browser. The cap is
|
|
4551
|
+
* generous rather than exact - older recorders and hand-written API
|
|
4552
|
+
* callers exist - but it is a cap: an unbounded caller-supplied string
|
|
4553
|
+
* reaches ClickHouse as a bound parameter on a hot path.
|
|
4554
|
+
*/
|
|
4555
|
+
const MAX_SESSION_REPLAY_SESSION_ID_LENGTH: number = 128;
|
|
4556
|
+
|
|
4107
4557
|
const readSessionIdFromBody: ReadSessionIdFromBodyFunction = (
|
|
4108
4558
|
body: JSONObject,
|
|
4109
4559
|
): string => {
|
|
@@ -4113,9 +4563,34 @@ const readSessionIdFromBody: ReadSessionIdFromBodyFunction = (
|
|
|
4113
4563
|
throw new BadDataException("sessionId is required");
|
|
4114
4564
|
}
|
|
4115
4565
|
|
|
4566
|
+
if (sessionId.length > MAX_SESSION_REPLAY_SESSION_ID_LENGTH) {
|
|
4567
|
+
throw new BadDataException(
|
|
4568
|
+
`sessionId must be at most ${MAX_SESSION_REPLAY_SESSION_ID_LENGTH} characters.`,
|
|
4569
|
+
);
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4116
4572
|
return sessionId;
|
|
4117
4573
|
};
|
|
4118
4574
|
|
|
4575
|
+
/*
|
|
4576
|
+
* Caps on the list's filter inputs.
|
|
4577
|
+
*
|
|
4578
|
+
* Every array filter becomes an `IN (...)` inside a HAVING, which
|
|
4579
|
+
* ClickHouse evaluates per GROUP over the whole window - so the cost of a
|
|
4580
|
+
* request is the caller's array length times the number of sessions in
|
|
4581
|
+
* range. Uncapped, one request carrying tens of thousands of browser names
|
|
4582
|
+
* is a cheap denial of service against the list for any holder of the list
|
|
4583
|
+
* permission. The limits are far above any real UI: the filter panel
|
|
4584
|
+
* offers a couple of dozen browsers, and no session has more than a
|
|
4585
|
+
* handful of routes.
|
|
4586
|
+
*
|
|
4587
|
+
* Values are TRUNCATED and the array is SLICED rather than refused: an
|
|
4588
|
+
* over-long value simply cannot match anything a bounded column holds, so
|
|
4589
|
+
* a 400 would add nothing but a confusing error.
|
|
4590
|
+
*/
|
|
4591
|
+
const MAX_SESSION_REPLAY_FILTER_ARRAY_LENGTH: number = 50;
|
|
4592
|
+
const MAX_SESSION_REPLAY_FILTER_VALUE_LENGTH: number = 256;
|
|
4593
|
+
|
|
4119
4594
|
type ReadStringArrayFromBodyFunction = (
|
|
4120
4595
|
body: JSONObject,
|
|
4121
4596
|
key: string,
|
|
@@ -4131,25 +4606,125 @@ const readStringArrayFromBody: ReadStringArrayFromBodyFunction = (
|
|
|
4131
4606
|
return undefined;
|
|
4132
4607
|
}
|
|
4133
4608
|
|
|
4134
|
-
const strings: Array<string> =
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4609
|
+
const strings: Array<string> = [];
|
|
4610
|
+
|
|
4611
|
+
for (const item of value) {
|
|
4612
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
4613
|
+
continue;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
strings.push(item.substring(0, MAX_SESSION_REPLAY_FILTER_VALUE_LENGTH));
|
|
4617
|
+
|
|
4618
|
+
if (strings.length >= MAX_SESSION_REPLAY_FILTER_ARRAY_LENGTH) {
|
|
4619
|
+
break;
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4139
4622
|
|
|
4140
4623
|
return strings.length > 0 ? strings : undefined;
|
|
4141
4624
|
};
|
|
4142
4625
|
|
|
4143
|
-
|
|
4626
|
+
/*
|
|
4627
|
+
* A single bounded string filter off the body. Same reasoning as the array
|
|
4628
|
+
* cap above: the value is compared per group, and nothing a column holds
|
|
4629
|
+
* is longer than this.
|
|
4630
|
+
*/
|
|
4631
|
+
type ReadBoundedStringFromBodyFunction = (
|
|
4632
|
+
body: JSONObject,
|
|
4633
|
+
key: string,
|
|
4634
|
+
) => string | undefined;
|
|
4144
4635
|
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4636
|
+
const readBoundedStringFromBody: ReadBoundedStringFromBodyFunction = (
|
|
4637
|
+
body: JSONObject,
|
|
4638
|
+
key: string,
|
|
4639
|
+
): string | undefined => {
|
|
4640
|
+
const value: unknown = body[key];
|
|
4641
|
+
|
|
4642
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4643
|
+
return undefined;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
return value.substring(0, MAX_SESSION_REPLAY_FILTER_VALUE_LENGTH);
|
|
4647
|
+
};
|
|
4648
|
+
|
|
4649
|
+
/*
|
|
4650
|
+
* The tag filter: a plain object of string pairs, each side bounded by the
|
|
4651
|
+
* same caps the ingest applied when it stored the map, so a filter can
|
|
4652
|
+
* never be longer than a value that could match it. Anything else reads
|
|
4653
|
+
* as "no tag filter".
|
|
4654
|
+
*/
|
|
4655
|
+
type ReadTagFilterFromBodyFunction = (
|
|
4656
|
+
filters: JSONObject,
|
|
4657
|
+
) => Record<string, string> | undefined;
|
|
4658
|
+
|
|
4659
|
+
const readTagFilterFromBody: ReadTagFilterFromBodyFunction = (
|
|
4660
|
+
filters: JSONObject,
|
|
4661
|
+
): Record<string, string> | undefined => {
|
|
4662
|
+
const value: unknown = filters["tags"];
|
|
4663
|
+
|
|
4664
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4665
|
+
return undefined;
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
const tags: Record<string, string> = {};
|
|
4669
|
+
let count: number = 0;
|
|
4670
|
+
|
|
4671
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
4672
|
+
const entry: unknown = (value as Record<string, unknown>)[key];
|
|
4673
|
+
|
|
4674
|
+
if (typeof entry !== "string" || key.length === 0) {
|
|
4675
|
+
continue;
|
|
4676
|
+
}
|
|
4677
|
+
|
|
4678
|
+
if (count >= SESSION_REPLAY_MAX_TAG_KEYS) {
|
|
4679
|
+
break;
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
tags[key.substring(0, SESSION_REPLAY_MAX_TAG_KEY_LENGTH)] = entry.substring(
|
|
4683
|
+
0,
|
|
4684
|
+
SESSION_REPLAY_MAX_TAG_VALUE_LENGTH,
|
|
4685
|
+
);
|
|
4686
|
+
count++;
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
return count > 0 ? tags : undefined;
|
|
4690
|
+
};
|
|
4691
|
+
|
|
4692
|
+
/*
|
|
4693
|
+
* Can this cursor's sortValue actually be bound into a query?
|
|
4694
|
+
*
|
|
4695
|
+
* parseSessionReplayListCursor only checks Number.isFinite, which admits
|
|
4696
|
+
* 1e300. For the startTime ordering that value becomes `new Date(1e300)` -
|
|
4697
|
+
* an Invalid Date, which renders as NaN text and is rejected by the
|
|
4698
|
+
* ClickHouse driver, so a crafted or corrupted cursor answered 500 instead
|
|
4699
|
+
* of the 400 the handler promises. For the aggregate orderings the value is
|
|
4700
|
+
* a count or a duration, and a negative one belongs to no page.
|
|
4701
|
+
*/
|
|
4702
|
+
type IsBindableCursorSortValueFunction = (
|
|
4703
|
+
cursor: SessionReplayListCursor,
|
|
4704
|
+
sortBy: SessionReplaySortBy,
|
|
4705
|
+
) => boolean;
|
|
4706
|
+
|
|
4707
|
+
const isBindableCursorSortValue: IsBindableCursorSortValueFunction = (
|
|
4708
|
+
cursor: SessionReplayListCursor,
|
|
4709
|
+
sortBy: SessionReplaySortBy,
|
|
4710
|
+
): boolean => {
|
|
4711
|
+
if (sortBy === "startTime") {
|
|
4712
|
+
return Number.isFinite(new Date(cursor.sortValue).getTime());
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
return cursor.sortValue >= 0;
|
|
4716
|
+
};
|
|
4717
|
+
|
|
4718
|
+
// --- Session Replay List Endpoint ---
|
|
4719
|
+
|
|
4720
|
+
router.post(
|
|
4721
|
+
"/telemetry/rum/session-replay/list",
|
|
4722
|
+
...requireSessionReplayListAccess,
|
|
4723
|
+
async (
|
|
4724
|
+
req: ExpressRequest,
|
|
4725
|
+
res: ExpressResponse,
|
|
4726
|
+
next: NextFunction,
|
|
4727
|
+
): Promise<void> => {
|
|
4153
4728
|
try {
|
|
4154
4729
|
const databaseProps: DatabaseCommonInteractionProps =
|
|
4155
4730
|
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
@@ -4186,13 +4761,56 @@ router.post(
|
|
|
4186
4761
|
permissions: SESSION_REPLAY_LIST_PERMISSIONS,
|
|
4187
4762
|
});
|
|
4188
4763
|
|
|
4189
|
-
const startTime: Date =
|
|
4190
|
-
|
|
4191
|
-
|
|
4764
|
+
const startTime: Date =
|
|
4765
|
+
readOptionalDateFromBody(body, "startTime") ||
|
|
4766
|
+
OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -7);
|
|
4192
4767
|
|
|
4193
|
-
const endTime: Date =
|
|
4194
|
-
|
|
4195
|
-
|
|
4768
|
+
const endTime: Date =
|
|
4769
|
+
readOptionalDateFromBody(body, "endTime") ||
|
|
4770
|
+
OneUptimeDate.getCurrentDate();
|
|
4771
|
+
|
|
4772
|
+
/*
|
|
4773
|
+
* An inverted window matches nothing, and an empty list with a 200
|
|
4774
|
+
* is exactly the answer a client cannot distinguish from "no
|
|
4775
|
+
* sessions" - so it is a 400 with the reason instead.
|
|
4776
|
+
*/
|
|
4777
|
+
if (startTime.getTime() > endTime.getTime()) {
|
|
4778
|
+
return Response.sendErrorResponse(
|
|
4779
|
+
req,
|
|
4780
|
+
res,
|
|
4781
|
+
new BadDataException("startTime must not be after endTime"),
|
|
4782
|
+
);
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
const limit: number = readLimitFromBody(
|
|
4786
|
+
body,
|
|
4787
|
+
DEFAULT_SESSION_REPLAY_LIST_LIMIT,
|
|
4788
|
+
MAX_SESSION_REPLAY_LIST_LIMIT,
|
|
4789
|
+
);
|
|
4790
|
+
|
|
4791
|
+
const rawSortBy: unknown = body["sortBy"];
|
|
4792
|
+
|
|
4793
|
+
if (
|
|
4794
|
+
rawSortBy !== undefined &&
|
|
4795
|
+
rawSortBy !== null &&
|
|
4796
|
+
(typeof rawSortBy !== "string" ||
|
|
4797
|
+
!(SESSION_REPLAY_SORT_BY_VALUES as ReadonlyArray<string>).includes(
|
|
4798
|
+
rawSortBy,
|
|
4799
|
+
))
|
|
4800
|
+
) {
|
|
4801
|
+
return Response.sendErrorResponse(
|
|
4802
|
+
req,
|
|
4803
|
+
res,
|
|
4804
|
+
new BadDataException(
|
|
4805
|
+
`sortBy must be one of ${SESSION_REPLAY_SORT_BY_VALUES.join(", ")}.`,
|
|
4806
|
+
),
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
const sortBy: SessionReplaySortBy =
|
|
4811
|
+
typeof rawSortBy === "string"
|
|
4812
|
+
? (rawSortBy as SessionReplaySortBy)
|
|
4813
|
+
: "startTime";
|
|
4196
4814
|
|
|
4197
4815
|
/*
|
|
4198
4816
|
* The narrower identity ACL, resolved BEFORE the filters are built.
|
|
@@ -4239,6 +4857,32 @@ router.post(
|
|
|
4239
4857
|
);
|
|
4240
4858
|
}
|
|
4241
4859
|
|
|
4860
|
+
/*
|
|
4861
|
+
* A filter the server understood but did not apply, named in the
|
|
4862
|
+
* response.
|
|
4863
|
+
*
|
|
4864
|
+
* The identity filter is gated on the narrower identity permission.
|
|
4865
|
+
* Dropping it silently is the dangerous half of that gate: the
|
|
4866
|
+
* request "show me jane@example.com's sessions" then answers 200 with
|
|
4867
|
+
* EVERY session in the application, and the caller has no way to tell
|
|
4868
|
+
* - least of all when the application has no sessions at all, where
|
|
4869
|
+
* even inspecting the rows cannot reveal it. A support engineer opens
|
|
4870
|
+
* the wrong recording, and each opening writes an audit row against a
|
|
4871
|
+
* real end user.
|
|
4872
|
+
*
|
|
4873
|
+
* Answered as data rather than as a 403 because the rest of the list
|
|
4874
|
+
* IS something the caller may read: the page renders the sessions and
|
|
4875
|
+
* says the filter was ignored, instead of failing outright.
|
|
4876
|
+
*/
|
|
4877
|
+
const ignoredFilters: Array<string> = [];
|
|
4878
|
+
|
|
4879
|
+
if (
|
|
4880
|
+
!includeIdentifiedUserLabel &&
|
|
4881
|
+
SessionReplayIdentity.isUsableUserRef(rawFilters["identifiedUserRef"])
|
|
4882
|
+
) {
|
|
4883
|
+
ignoredFilters.push("identifiedUserRef");
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4242
4886
|
const filters: SessionReplayListFilters = {
|
|
4243
4887
|
...(typeof rawFilters["hasError"] === "boolean" && {
|
|
4244
4888
|
hasError: rawFilters["hasError"],
|
|
@@ -4294,33 +4938,116 @@ router.post(
|
|
|
4294
4938
|
* not guessable and is already returned to every list-capable
|
|
4295
4939
|
* caller, so it needs no identity gate of its own.
|
|
4296
4940
|
*/
|
|
4297
|
-
...(
|
|
4941
|
+
...(readBoundedStringFromBody(rawFilters, "identifiedUserKey") &&
|
|
4298
4942
|
!SessionReplayIdentity.isUsableUserRef(
|
|
4299
4943
|
rawFilters["identifiedUserRef"],
|
|
4300
4944
|
) && {
|
|
4301
|
-
identifiedUserKey:
|
|
4945
|
+
identifiedUserKey: readBoundedStringFromBody(
|
|
4946
|
+
rawFilters,
|
|
4947
|
+
"identifiedUserKey",
|
|
4948
|
+
),
|
|
4302
4949
|
}),
|
|
4303
|
-
...(
|
|
4304
|
-
route: rawFilters
|
|
4950
|
+
...(readBoundedStringFromBody(rawFilters, "route") && {
|
|
4951
|
+
route: readBoundedStringFromBody(rawFilters, "route"),
|
|
4305
4952
|
}),
|
|
4306
|
-
...(typeof rawFilters["minDurationMs"] === "number" &&
|
|
4307
|
-
|
|
4953
|
+
...(typeof rawFilters["minDurationMs"] === "number" &&
|
|
4954
|
+
Number.isFinite(rawFilters["minDurationMs"]) && {
|
|
4955
|
+
minDurationMs: rawFilters["minDurationMs"],
|
|
4956
|
+
}),
|
|
4957
|
+
...(typeof rawFilters["hasIdentifiedUser"] === "boolean" && {
|
|
4958
|
+
hasIdentifiedUser: rawFilters["hasIdentifiedUser"],
|
|
4959
|
+
}),
|
|
4960
|
+
...(typeof rawFilters["isPlayable"] === "boolean" && {
|
|
4961
|
+
isPlayable: rawFilters["isPlayable"],
|
|
4962
|
+
}),
|
|
4963
|
+
...(typeof rawFilters["hasTraces"] === "boolean" && {
|
|
4964
|
+
hasTraces: rawFilters["hasTraces"],
|
|
4965
|
+
}),
|
|
4966
|
+
...(typeof rawFilters["urlPrefix"] === "string" &&
|
|
4967
|
+
rawFilters["urlPrefix"].length > 0 && {
|
|
4968
|
+
urlPrefix: rawFilters["urlPrefix"].substring(
|
|
4969
|
+
0,
|
|
4970
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH,
|
|
4971
|
+
),
|
|
4972
|
+
}),
|
|
4973
|
+
...(readTagFilterFromBody(rawFilters) && {
|
|
4974
|
+
tags: readTagFilterFromBody(rawFilters),
|
|
4308
4975
|
}),
|
|
4309
4976
|
};
|
|
4310
4977
|
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4978
|
+
/*
|
|
4979
|
+
* Free text is the one predicate that cannot use an index: it is a
|
|
4980
|
+
* substring scan of every header in the window. The string cap
|
|
4981
|
+
* keeps each comparison cheap; the window cap keeps the number of
|
|
4982
|
+
* comparisons bounded, and is answered with its own message so the
|
|
4983
|
+
* list can say "narrow the range" rather than "no sessions".
|
|
4984
|
+
*/
|
|
4985
|
+
const rawSearch: unknown = rawFilters["search"];
|
|
4314
4986
|
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4987
|
+
if (rawSearch !== undefined && rawSearch !== null && rawSearch !== "") {
|
|
4988
|
+
if (typeof rawSearch !== "string") {
|
|
4989
|
+
return Response.sendErrorResponse(
|
|
4990
|
+
req,
|
|
4991
|
+
res,
|
|
4992
|
+
new BadDataException("search must be a string"),
|
|
4993
|
+
);
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4996
|
+
if (rawSearch.length > SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH) {
|
|
4997
|
+
return Response.sendErrorResponse(
|
|
4998
|
+
req,
|
|
4999
|
+
res,
|
|
5000
|
+
new BadDataException(
|
|
5001
|
+
`search must be at most ${SESSION_REPLAY_LIST_SEARCH_MAX_LENGTH} characters.`,
|
|
5002
|
+
),
|
|
5003
|
+
);
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
const windowMs: number = endTime.getTime() - startTime.getTime();
|
|
5007
|
+
|
|
5008
|
+
if (
|
|
5009
|
+
windowMs >
|
|
5010
|
+
SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS * 24 * 60 * 60 * 1000
|
|
5011
|
+
) {
|
|
5012
|
+
return Response.sendErrorResponse(
|
|
5013
|
+
req,
|
|
5014
|
+
res,
|
|
5015
|
+
new BadDataException(
|
|
5016
|
+
`Search covers at most ${SESSION_REPLAY_LIST_SEARCH_MAX_WINDOW_DAYS} days at a time. Narrow the range to search it.`,
|
|
5017
|
+
),
|
|
5018
|
+
);
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
if (rawSearch.trim().length > 0) {
|
|
5022
|
+
filters.search = rawSearch.trim();
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
/*
|
|
5027
|
+
* Both cursor shapes are accepted: the legacy {startTimeUnixMs,
|
|
5028
|
+
* sessionId} one an older Dashboard or a bookmark still sends (it
|
|
5029
|
+
* means "newest first"), and the sorted one. A cursor for a
|
|
5030
|
+
* different ordering than the one requested is refused by the
|
|
5031
|
+
* service rather than silently mis-paged.
|
|
5032
|
+
*/
|
|
5033
|
+
const cursor: SessionReplayListCursor | null =
|
|
5034
|
+
body["cursor"] !== undefined && body["cursor"] !== null
|
|
5035
|
+
? parseSessionReplayListCursor(body["cursor"])
|
|
5036
|
+
: null;
|
|
5037
|
+
|
|
5038
|
+
if (
|
|
5039
|
+
body["cursor"] !== undefined &&
|
|
5040
|
+
body["cursor"] !== null &&
|
|
5041
|
+
(cursor === null || !isBindableCursorSortValue(cursor, sortBy))
|
|
5042
|
+
) {
|
|
5043
|
+
return Response.sendErrorResponse(
|
|
5044
|
+
req,
|
|
5045
|
+
res,
|
|
5046
|
+
new BadDataException(
|
|
5047
|
+
"cursor must be the nextCursor of a previous page.",
|
|
5048
|
+
),
|
|
5049
|
+
);
|
|
5050
|
+
}
|
|
4324
5051
|
|
|
4325
5052
|
const result: SessionReplayListResult =
|
|
4326
5053
|
await SessionReplayReadService.listSessions({
|
|
@@ -4329,17 +5056,39 @@ router.post(
|
|
|
4329
5056
|
startTime: startTime,
|
|
4330
5057
|
endTime: endTime,
|
|
4331
5058
|
filters: filters,
|
|
4332
|
-
limit:
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
: DEFAULT_SESSION_REPLAY_LIST_LIMIT,
|
|
4336
|
-
...(cursor !== undefined && { cursor }),
|
|
5059
|
+
limit: limit,
|
|
5060
|
+
sortBy: sortBy,
|
|
5061
|
+
...(cursor !== null && { cursor }),
|
|
4337
5062
|
includeIdentifiedUserLabel: includeIdentifiedUserLabel,
|
|
4338
5063
|
});
|
|
4339
5064
|
|
|
5065
|
+
/*
|
|
5066
|
+
* The legacy cursor shape is still EMITTED for the newest-first sort
|
|
5067
|
+
* so an older Dashboard keeps paging; every other sort emits the
|
|
5068
|
+
* generalised shape, which such a client never asks for.
|
|
5069
|
+
*/
|
|
5070
|
+
const nextCursor: JSONObject | null = result.nextCursor
|
|
5071
|
+
? sortBy === "startTime"
|
|
5072
|
+
? {
|
|
5073
|
+
startTimeUnixMs: result.nextCursor.sortValue,
|
|
5074
|
+
sessionId: result.nextCursor.sessionId,
|
|
5075
|
+
}
|
|
5076
|
+
: {
|
|
5077
|
+
sortBy: result.nextCursor.sortBy,
|
|
5078
|
+
sortValue: result.nextCursor.sortValue,
|
|
5079
|
+
sessionId: result.nextCursor.sessionId,
|
|
5080
|
+
}
|
|
5081
|
+
: null;
|
|
5082
|
+
|
|
4340
5083
|
return Response.sendJsonObjectResponse(req, res, {
|
|
4341
5084
|
sessions: result.sessions as unknown as JSONObject,
|
|
4342
|
-
nextCursor:
|
|
5085
|
+
nextCursor: nextCursor,
|
|
5086
|
+
/*
|
|
5087
|
+
* Always present, empty when everything asked for was applied, so a
|
|
5088
|
+
* client can read it without having to distinguish "no ignored
|
|
5089
|
+
* filters" from "an older server that never said".
|
|
5090
|
+
*/
|
|
5091
|
+
ignoredFilters: ignoredFilters as unknown as JSONArray,
|
|
4343
5092
|
});
|
|
4344
5093
|
} catch (err: unknown) {
|
|
4345
5094
|
next(err);
|
|
@@ -4375,13 +5124,21 @@ router.post(
|
|
|
4375
5124
|
const body: JSONObject = req.body as JSONObject;
|
|
4376
5125
|
const sessionId: string = readSessionIdFromBody(body);
|
|
4377
5126
|
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
5127
|
+
/*
|
|
5128
|
+
* Optional disambiguator, validated but NOT trusted: the header it
|
|
5129
|
+
* selects still names the application that gets authorized.
|
|
5130
|
+
*/
|
|
5131
|
+
const requestedApplicationId: ObjectID | undefined =
|
|
5132
|
+
readOptionalObjectIdFromBody(body, "rumApplicationId");
|
|
5133
|
+
|
|
5134
|
+
const authorized: AuthorizedSession = await resolveAuthorizedSession({
|
|
5135
|
+
projectId: projectId,
|
|
5136
|
+
sessionId: sessionId,
|
|
5137
|
+
databaseProps: databaseProps,
|
|
5138
|
+
rumApplicationId: requestedApplicationId,
|
|
5139
|
+
});
|
|
5140
|
+
|
|
5141
|
+
const header: SessionReplaySessionHeader = authorized.header;
|
|
4385
5142
|
|
|
4386
5143
|
/*
|
|
4387
5144
|
* The authorized application, resolved server-side from the header.
|
|
@@ -4395,52 +5152,135 @@ router.post(
|
|
|
4395
5152
|
);
|
|
4396
5153
|
|
|
4397
5154
|
/*
|
|
4398
|
-
*
|
|
4399
|
-
*
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
5155
|
+
* A live-session poll reuses the view row the first read created.
|
|
5156
|
+
*
|
|
5157
|
+
* The player re-fetches the manifest every 30s while a session is
|
|
5158
|
+
* still recording, and each of those is the same person continuing
|
|
5159
|
+
* the same viewing, not a new disclosure. Writing a row per poll
|
|
5160
|
+
* would turn one viewing into dozens of audit entries and bury the
|
|
5161
|
+
* signal the audit exists for. The row is reused ONLY when the
|
|
5162
|
+
* caller proves it is theirs (looked up by viewedByUserId) AND it
|
|
5163
|
+
* is for this session; anything else - somebody else's viewId, a
|
|
5164
|
+
* different session, an id that no longer exists - is a fresh
|
|
5165
|
+
* recordView, exactly as if isRefresh had not been sent.
|
|
4402
5166
|
*/
|
|
4403
|
-
|
|
4404
|
-
|
|
5167
|
+
let viewId: string | null = null;
|
|
5168
|
+
|
|
5169
|
+
const isRefresh: boolean = body["isRefresh"] === true;
|
|
5170
|
+
const refreshViewId: ObjectID | undefined = isRefresh
|
|
5171
|
+
? readOptionalObjectIdFromBody(body, "viewId")
|
|
5172
|
+
: undefined;
|
|
5173
|
+
|
|
5174
|
+
if (refreshViewId && databaseProps.userId) {
|
|
5175
|
+
const ownView: RumSessionReplayView | null =
|
|
5176
|
+
await RumSessionReplayViewService.findOwnView({
|
|
5177
|
+
viewId: refreshViewId,
|
|
5178
|
+
projectId: projectId,
|
|
5179
|
+
viewedByUserId: databaseProps.userId,
|
|
5180
|
+
sessionId: sessionId,
|
|
5181
|
+
});
|
|
5182
|
+
|
|
5183
|
+
if (
|
|
5184
|
+
ownView &&
|
|
5185
|
+
ownView.id &&
|
|
5186
|
+
ownView.rumApplicationId &&
|
|
5187
|
+
ownView.rumApplicationId.toString() ===
|
|
5188
|
+
authorizedApplicationId.toString()
|
|
5189
|
+
) {
|
|
5190
|
+
viewId = ownView.id.toString();
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
if (viewId === null) {
|
|
5195
|
+
/*
|
|
5196
|
+
* The audit row is written BEFORE the manifest is built, not
|
|
5197
|
+
* after. A read that fails halfway through still happened, and
|
|
5198
|
+
* an audit that only records successful reads is an audit an
|
|
5199
|
+
* attacker can evade by aborting the request.
|
|
5200
|
+
*
|
|
5201
|
+
* linkedIncidentId is validated rather than constructed blindly:
|
|
5202
|
+
* ObjectID's constructor checks nothing, and a malformed id would
|
|
5203
|
+
* fail the audit insert - and with it the playback it audits. A
|
|
5204
|
+
* bad link is dropped; the view is still recorded.
|
|
5205
|
+
*/
|
|
5206
|
+
const rawLinkedIncidentId: unknown = body["linkedIncidentId"];
|
|
5207
|
+
|
|
5208
|
+
const view: RumSessionReplayView =
|
|
5209
|
+
await RumSessionReplayViewService.recordView({
|
|
5210
|
+
projectId: projectId,
|
|
5211
|
+
rumApplicationId: authorizedApplicationId,
|
|
5212
|
+
sessionId: sessionId,
|
|
5213
|
+
viewedByUserId: databaseProps.userId,
|
|
5214
|
+
ipAddress: getClientIp(req),
|
|
5215
|
+
userAgent:
|
|
5216
|
+
typeof req.headers["user-agent"] === "string"
|
|
5217
|
+
? req.headers["user-agent"]
|
|
5218
|
+
: undefined,
|
|
5219
|
+
accessReason:
|
|
5220
|
+
typeof body["accessReason"] === "string"
|
|
5221
|
+
? (body["accessReason"] as string)
|
|
5222
|
+
: undefined,
|
|
5223
|
+
linkedIncidentId:
|
|
5224
|
+
typeof rawLinkedIncidentId === "string" &&
|
|
5225
|
+
ObjectID.isValidUUID(rawLinkedIncidentId)
|
|
5226
|
+
? new ObjectID(rawLinkedIncidentId)
|
|
5227
|
+
: undefined,
|
|
5228
|
+
linkedExceptionFingerprint:
|
|
5229
|
+
typeof body["linkedExceptionFingerprint"] === "string" &&
|
|
5230
|
+
body["linkedExceptionFingerprint"].length > 0
|
|
5231
|
+
? (body["linkedExceptionFingerprint"] as string)
|
|
5232
|
+
: undefined,
|
|
5233
|
+
});
|
|
5234
|
+
|
|
5235
|
+
viewId = view.id ? view.id.toString() : null;
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
const manifest: SessionReplayManifest =
|
|
5239
|
+
await SessionReplayReadService.getManifest({
|
|
5240
|
+
header: header,
|
|
4405
5241
|
projectId: projectId,
|
|
4406
5242
|
rumApplicationId: authorizedApplicationId,
|
|
4407
5243
|
sessionId: sessionId,
|
|
4408
|
-
viewedByUserId: databaseProps.userId,
|
|
4409
|
-
ipAddress: getClientIp(req),
|
|
4410
|
-
userAgent:
|
|
4411
|
-
typeof req.headers["user-agent"] === "string"
|
|
4412
|
-
? req.headers["user-agent"]
|
|
4413
|
-
: undefined,
|
|
4414
|
-
accessReason:
|
|
4415
|
-
typeof body["accessReason"] === "string"
|
|
4416
|
-
? (body["accessReason"] as string)
|
|
4417
|
-
: undefined,
|
|
4418
|
-
linkedIncidentId:
|
|
4419
|
-
typeof body["linkedIncidentId"] === "string" &&
|
|
4420
|
-
body["linkedIncidentId"].length > 0
|
|
4421
|
-
? new ObjectID(body["linkedIncidentId"])
|
|
4422
|
-
: undefined,
|
|
4423
|
-
linkedExceptionFingerprint:
|
|
4424
|
-
typeof body["linkedExceptionFingerprint"] === "string"
|
|
4425
|
-
? (body["linkedExceptionFingerprint"] as string)
|
|
4426
|
-
: undefined,
|
|
4427
5244
|
});
|
|
4428
5245
|
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
5246
|
+
/*
|
|
5247
|
+
* The identity columns are read by a SEPARATE statement that runs
|
|
5248
|
+
* only after the narrower identity check passes for the application
|
|
5249
|
+
* the caller was just authorized against. Nothing above named them,
|
|
5250
|
+
* so a caller without the permission never causes a statement that
|
|
5251
|
+
* touches identifiedUserLabel or identifiedUserTraits to exist.
|
|
5252
|
+
*/
|
|
5253
|
+
let identity: SessionReplaySessionIdentity | null = null;
|
|
5254
|
+
|
|
5255
|
+
if (
|
|
5256
|
+
canReadIdentifiedUserLabel({
|
|
5257
|
+
databaseProps: databaseProps,
|
|
5258
|
+
application: authorized.application,
|
|
5259
|
+
})
|
|
5260
|
+
) {
|
|
5261
|
+
identity = await SessionReplayReadService.getSessionIdentity({
|
|
4432
5262
|
projectId: projectId,
|
|
4433
5263
|
rumApplicationId: authorizedApplicationId,
|
|
4434
5264
|
sessionId: sessionId,
|
|
4435
5265
|
});
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
const responseHeader: SessionReplaySessionHeader = identity
|
|
5269
|
+
? {
|
|
5270
|
+
...manifest.header,
|
|
5271
|
+
identifiedUserLabel: identity.identifiedUserLabel,
|
|
5272
|
+
identifiedUserTraits: identity.identifiedUserTraits,
|
|
5273
|
+
}
|
|
5274
|
+
: manifest.header;
|
|
4436
5275
|
|
|
4437
5276
|
return Response.sendJsonObjectResponse(req, res, {
|
|
4438
5277
|
/*
|
|
4439
5278
|
* viewId is echoed back so the player's heartbeat can advance the
|
|
4440
|
-
* very row this read created, instead of guessing at
|
|
5279
|
+
* very row this read created (or reused), instead of guessing at
|
|
5280
|
+
* one.
|
|
4441
5281
|
*/
|
|
4442
|
-
viewId:
|
|
4443
|
-
header:
|
|
5282
|
+
viewId: viewId,
|
|
5283
|
+
header: responseHeader as unknown as JSONObject,
|
|
4444
5284
|
tabs: manifest.tabs as unknown as JSONObject,
|
|
4445
5285
|
isChunkIndexTruncated: manifest.isChunkIndexTruncated,
|
|
4446
5286
|
});
|
|
@@ -4532,15 +5372,27 @@ router.post(
|
|
|
4532
5372
|
);
|
|
4533
5373
|
}
|
|
4534
5374
|
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
5375
|
+
/*
|
|
5376
|
+
* Served from the 30s authorization cache: a session is 60 chunk
|
|
5377
|
+
* pages at most, and re-aggregating its header plus re-loading the
|
|
5378
|
+
* application's labels for every one of them is what made seeks
|
|
5379
|
+
* stutter. The label decision itself is still made against the
|
|
5380
|
+
* caller's current permissions on every request.
|
|
5381
|
+
*/
|
|
5382
|
+
const authorized: AuthorizedSession = await resolveAuthorizedSession({
|
|
5383
|
+
projectId: projectId,
|
|
5384
|
+
sessionId: sessionId,
|
|
5385
|
+
databaseProps: databaseProps,
|
|
5386
|
+
rumApplicationId: readOptionalObjectIdFromBody(
|
|
5387
|
+
body,
|
|
5388
|
+
"rumApplicationId",
|
|
5389
|
+
),
|
|
5390
|
+
allowCached: true,
|
|
5391
|
+
});
|
|
5392
|
+
|
|
5393
|
+
const header: SessionReplaySessionHeader = authorized.header;
|
|
4542
5394
|
|
|
4543
|
-
const
|
|
5395
|
+
const read: SessionReplayChunkReadResult =
|
|
4544
5396
|
await SessionReplayReadService.getChunks({
|
|
4545
5397
|
projectId: projectId,
|
|
4546
5398
|
/* Always the application the caller was authorized against. */
|
|
@@ -4563,28 +5415,33 @@ router.post(
|
|
|
4563
5415
|
|
|
4564
5416
|
/*
|
|
4565
5417
|
* The byte cap is re-checked here against the bytes actually being
|
|
4566
|
-
* framed, not only against what the
|
|
4567
|
-
*
|
|
4568
|
-
*
|
|
4569
|
-
*
|
|
5418
|
+
* framed, not only against what the read service believed. This is
|
|
5419
|
+
* the last place the size of the response is knowable, so it is the
|
|
5420
|
+
* one place a cap on the response can be absolute regardless of how
|
|
5421
|
+
* stored size was estimated upstream. Like the service, it answers
|
|
5422
|
+
* with the prefix that fits rather than refusing - and never with
|
|
5423
|
+
* nothing: a single chunk is bounded by the ingest cap, and a chunk
|
|
5424
|
+
* that could never be served would dead-end playback at it forever.
|
|
4570
5425
|
*/
|
|
4571
5426
|
let responseBytes: number = 0;
|
|
5427
|
+
const omittedChunkIndexes: Array<number> = [...read.omittedChunkIndexes];
|
|
4572
5428
|
|
|
4573
|
-
for (const chunk of chunks) {
|
|
5429
|
+
for (const chunk of read.chunks) {
|
|
4574
5430
|
const payloadBuffer: Buffer = Buffer.from(chunk.payload, "utf8");
|
|
4575
5431
|
|
|
4576
|
-
|
|
5432
|
+
const framedBytes: number = payloadBuffer.length + 8;
|
|
4577
5433
|
|
|
4578
|
-
if (
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
);
|
|
5434
|
+
if (
|
|
5435
|
+
frames.length > 0 &&
|
|
5436
|
+
(responseBytes + framedBytes > MAX_SESSION_REPLAY_READ_BYTES ||
|
|
5437
|
+
omittedChunkIndexes.length > 0)
|
|
5438
|
+
) {
|
|
5439
|
+
omittedChunkIndexes.push(chunk.chunkIndex);
|
|
5440
|
+
continue;
|
|
4586
5441
|
}
|
|
4587
5442
|
|
|
5443
|
+
responseBytes += framedBytes;
|
|
5444
|
+
|
|
4588
5445
|
const headerBuffer: Buffer = Buffer.alloc(8);
|
|
4589
5446
|
headerBuffer.writeUInt32LE(chunk.chunkIndex, 0);
|
|
4590
5447
|
headerBuffer.writeUInt32LE(payloadBuffer.length, 4);
|
|
@@ -4595,6 +5452,22 @@ router.post(
|
|
|
4595
5452
|
|
|
4596
5453
|
res.setHeader("Content-Type", "application/octet-stream");
|
|
4597
5454
|
res.setHeader("Content-Length", responseBody.length.toString());
|
|
5455
|
+
|
|
5456
|
+
/*
|
|
5457
|
+
* Which requested chunks exist but were left out for size, so a
|
|
5458
|
+
* client that cares can ask for them in a smaller page instead of
|
|
5459
|
+
* reading their absence as a gap in the recording.
|
|
5460
|
+
*/
|
|
5461
|
+
if (omittedChunkIndexes.length > 0) {
|
|
5462
|
+
res.setHeader(
|
|
5463
|
+
"X-OneUptime-Replay-Omitted-Chunks",
|
|
5464
|
+
omittedChunkIndexes
|
|
5465
|
+
.sort((a: number, b: number): number => {
|
|
5466
|
+
return a - b;
|
|
5467
|
+
})
|
|
5468
|
+
.join(","),
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
4598
5471
|
/*
|
|
4599
5472
|
* A recording is personal data. Nothing about it may sit in a
|
|
4600
5473
|
* shared cache, and the browser should not keep it on disk either.
|
|
@@ -4650,14 +5523,16 @@ router.post(
|
|
|
4650
5523
|
}
|
|
4651
5524
|
|
|
4652
5525
|
/*
|
|
4653
|
-
*
|
|
4654
|
-
*
|
|
4655
|
-
*
|
|
4656
|
-
*
|
|
4657
|
-
*
|
|
5526
|
+
* SEMANTICS: secondsWatched is the cumulative seconds of footage the
|
|
5527
|
+
* player has PLAYED for this view (accumulated client-side while
|
|
5528
|
+
* playback runs, scaled by speed), not the furthest offset reached.
|
|
5529
|
+
* Floored to the 15s heartbeat cadence and clamped by the service,
|
|
5530
|
+
* which is monotonic and writes nothing when the value does not
|
|
5531
|
+
* advance, so a chatty client costs at most one UPDATE per 15
|
|
5532
|
+
* seconds watched without any server-side timer or cross-pod
|
|
5533
|
+
* throttle state.
|
|
4658
5534
|
*/
|
|
4659
|
-
const throttledSeconds: number =
|
|
4660
|
-
Math.floor(Math.max(0, secondsWatched) / 15) * 15;
|
|
5535
|
+
const throttledSeconds: number = normalizeSecondsWatched(secondsWatched);
|
|
4661
5536
|
|
|
4662
5537
|
/*
|
|
4663
5538
|
* secondsWatched is a privacy control, not telemetry: it is shown on
|
|
@@ -4671,7 +5546,8 @@ router.post(
|
|
|
4671
5546
|
* viewId belonging to anyone else matches nothing and is refused
|
|
4672
5547
|
* indistinguishably from one that does not exist - and the
|
|
4673
5548
|
* application it points at is authorized exactly as a payload read
|
|
4674
|
-
* would be.
|
|
5549
|
+
* would be. That ONE lookup also carries the row's current figure,
|
|
5550
|
+
* so a heartbeat that does not advance it ends here with no write.
|
|
4675
5551
|
*/
|
|
4676
5552
|
if (!databaseProps.userId) {
|
|
4677
5553
|
return Response.sendErrorResponse(
|
|
@@ -4684,19 +5560,10 @@ router.post(
|
|
|
4684
5560
|
}
|
|
4685
5561
|
|
|
4686
5562
|
const view: RumSessionReplayView | null =
|
|
4687
|
-
await RumSessionReplayViewService.
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
viewedByUserId: databaseProps.userId,
|
|
4692
|
-
},
|
|
4693
|
-
select: {
|
|
4694
|
-
_id: true,
|
|
4695
|
-
rumApplicationId: true,
|
|
4696
|
-
},
|
|
4697
|
-
props: {
|
|
4698
|
-
isRoot: true,
|
|
4699
|
-
},
|
|
5563
|
+
await RumSessionReplayViewService.findOwnView({
|
|
5564
|
+
viewId: viewId,
|
|
5565
|
+
projectId: projectId,
|
|
5566
|
+
viewedByUserId: databaseProps.userId,
|
|
4700
5567
|
});
|
|
4701
5568
|
|
|
4702
5569
|
if (!view || !view.rumApplicationId) {
|
|
@@ -4714,16 +5581,116 @@ router.post(
|
|
|
4714
5581
|
rumApplicationId: view.rumApplicationId,
|
|
4715
5582
|
databaseProps: databaseProps,
|
|
4716
5583
|
permissions: SESSION_REPLAY_PAYLOAD_PERMISSIONS,
|
|
5584
|
+
allowCached: true,
|
|
4717
5585
|
});
|
|
4718
5586
|
|
|
5587
|
+
const currentSecondsWatched: number = view.secondsWatched || 0;
|
|
5588
|
+
|
|
4719
5589
|
await RumSessionReplayViewService.recordSecondsWatched({
|
|
4720
5590
|
viewId: viewId,
|
|
4721
5591
|
projectId: projectId,
|
|
4722
5592
|
secondsWatched: throttledSeconds,
|
|
5593
|
+
currentSecondsWatched: currentSecondsWatched,
|
|
4723
5594
|
});
|
|
4724
5595
|
|
|
4725
5596
|
return Response.sendJsonObjectResponse(req, res, {
|
|
4726
|
-
secondsWatched: throttledSeconds,
|
|
5597
|
+
secondsWatched: Math.max(currentSecondsWatched, throttledSeconds),
|
|
5598
|
+
});
|
|
5599
|
+
} catch (err: unknown) {
|
|
5600
|
+
next(err);
|
|
5601
|
+
}
|
|
5602
|
+
},
|
|
5603
|
+
);
|
|
5604
|
+
|
|
5605
|
+
// --- Session Replay Views Endpoint ---
|
|
5606
|
+
|
|
5607
|
+
/*
|
|
5608
|
+
* Who has watched this recording. The audit table has been written on
|
|
5609
|
+
* every manifest read since the feature shipped and nothing surfaced it;
|
|
5610
|
+
* the player header now can. Payload permission, because the list of
|
|
5611
|
+
* viewers is a fact about a recording of a real person and belongs to
|
|
5612
|
+
* the people who may watch it. Pinned to the application the session was
|
|
5613
|
+
* authorized under: sessionId is only unique within an application.
|
|
5614
|
+
*/
|
|
5615
|
+
const MAX_SESSION_REPLAY_VIEWS_LIMIT: number = 50;
|
|
5616
|
+
|
|
5617
|
+
router.post(
|
|
5618
|
+
"/telemetry/rum/session-replay/views",
|
|
5619
|
+
...requireSessionReplayPayloadAccess,
|
|
5620
|
+
async (
|
|
5621
|
+
req: ExpressRequest,
|
|
5622
|
+
res: ExpressResponse,
|
|
5623
|
+
next: NextFunction,
|
|
5624
|
+
): Promise<void> => {
|
|
5625
|
+
try {
|
|
5626
|
+
const databaseProps: DatabaseCommonInteractionProps =
|
|
5627
|
+
await CommonAPI.getDatabaseCommonInteractionProps(req);
|
|
5628
|
+
|
|
5629
|
+
if (!databaseProps?.tenantId) {
|
|
5630
|
+
return Response.sendErrorResponse(
|
|
5631
|
+
req,
|
|
5632
|
+
res,
|
|
5633
|
+
new BadDataException("Invalid Project ID"),
|
|
5634
|
+
);
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
assertSessionReplayPlan(databaseProps);
|
|
5638
|
+
|
|
5639
|
+
const projectId: ObjectID = databaseProps.tenantId;
|
|
5640
|
+
const body: JSONObject = req.body as JSONObject;
|
|
5641
|
+
const sessionId: string = readSessionIdFromBody(body);
|
|
5642
|
+
|
|
5643
|
+
const authorized: AuthorizedSession = await resolveAuthorizedSession({
|
|
5644
|
+
projectId: projectId,
|
|
5645
|
+
sessionId: sessionId,
|
|
5646
|
+
databaseProps: databaseProps,
|
|
5647
|
+
rumApplicationId: readOptionalObjectIdFromBody(
|
|
5648
|
+
body,
|
|
5649
|
+
"rumApplicationId",
|
|
5650
|
+
),
|
|
5651
|
+
allowCached: true,
|
|
5652
|
+
});
|
|
5653
|
+
|
|
5654
|
+
const views: Array<RumSessionReplayView> =
|
|
5655
|
+
await RumSessionReplayViewService.getViewsForSession({
|
|
5656
|
+
projectId: projectId,
|
|
5657
|
+
rumApplicationId: new ObjectID(authorized.header.rumApplicationId),
|
|
5658
|
+
sessionId: sessionId,
|
|
5659
|
+
limit: readLimitFromBody(
|
|
5660
|
+
body,
|
|
5661
|
+
MAX_SESSION_REPLAY_VIEWS_LIMIT,
|
|
5662
|
+
MAX_SESSION_REPLAY_VIEWS_LIMIT,
|
|
5663
|
+
),
|
|
5664
|
+
});
|
|
5665
|
+
|
|
5666
|
+
return Response.sendJsonObjectResponse(req, res, {
|
|
5667
|
+
views: views.map((view: RumSessionReplayView): JSONObject => {
|
|
5668
|
+
return {
|
|
5669
|
+
id: view.id ? view.id.toString() : null,
|
|
5670
|
+
viewedAt: view.viewedAt ? view.viewedAt.toISOString() : null,
|
|
5671
|
+
secondsWatched: view.secondsWatched ?? 0,
|
|
5672
|
+
accessReason: view.accessReason || "",
|
|
5673
|
+
viewedByUserId: view.viewedByUserId
|
|
5674
|
+
? view.viewedByUserId.toString()
|
|
5675
|
+
: null,
|
|
5676
|
+
viewedByUser: view.viewedByUser
|
|
5677
|
+
? {
|
|
5678
|
+
id: view.viewedByUser.id
|
|
5679
|
+
? view.viewedByUser.id.toString()
|
|
5680
|
+
: null,
|
|
5681
|
+
name: view.viewedByUser.name
|
|
5682
|
+
? view.viewedByUser.name.toString()
|
|
5683
|
+
: "",
|
|
5684
|
+
email: view.viewedByUser.email
|
|
5685
|
+
? view.viewedByUser.email.toString()
|
|
5686
|
+
: "",
|
|
5687
|
+
profilePictureId: view.viewedByUser.profilePictureId
|
|
5688
|
+
? view.viewedByUser.profilePictureId.toString()
|
|
5689
|
+
: null,
|
|
5690
|
+
}
|
|
5691
|
+
: null,
|
|
5692
|
+
};
|
|
5693
|
+
}) as unknown as JSONObject,
|
|
4727
5694
|
});
|
|
4728
5695
|
} catch (err: unknown) {
|
|
4729
5696
|
next(err);
|
|
@@ -4780,13 +5747,49 @@ router.post(
|
|
|
4780
5747
|
permissions: SESSION_REPLAY_LIST_PERMISSIONS,
|
|
4781
5748
|
});
|
|
4782
5749
|
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
5750
|
+
let startTime: Date | undefined = readOptionalDateFromBody(
|
|
5751
|
+
body,
|
|
5752
|
+
"startTime",
|
|
5753
|
+
);
|
|
5754
|
+
let endTime: Date | undefined = readOptionalDateFromBody(body, "endTime");
|
|
4786
5755
|
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
5756
|
+
/*
|
|
5757
|
+
* The exception page knows WHEN the error happened. A session that
|
|
5758
|
+
* contains that moment started at most one maximum session length
|
|
5759
|
+
* before it (plus skew padding), so the window is derived from the
|
|
5760
|
+
* moment rather than left to the 30-day default - a fraction of
|
|
5761
|
+
* the partitions, and the card finds the session of THIS instance
|
|
5762
|
+
* rather than the newest session that ever hit the fingerprint.
|
|
5763
|
+
*/
|
|
5764
|
+
const rawErrorTime: unknown = body["errorTimeUnixMs"];
|
|
5765
|
+
|
|
5766
|
+
if (
|
|
5767
|
+
startTime === undefined &&
|
|
5768
|
+
endTime === undefined &&
|
|
5769
|
+
typeof rawErrorTime === "number" &&
|
|
5770
|
+
Number.isFinite(rawErrorTime) &&
|
|
5771
|
+
rawErrorTime > 0
|
|
5772
|
+
) {
|
|
5773
|
+
startTime = new Date(
|
|
5774
|
+
rawErrorTime -
|
|
5775
|
+
SESSION_REPLAY_MAX_SESSION_MS -
|
|
5776
|
+
SESSION_REPLAY_EXCEPTION_WINDOW_PADDING_MS,
|
|
5777
|
+
);
|
|
5778
|
+
endTime = new Date(
|
|
5779
|
+
rawErrorTime + SESSION_REPLAY_EXCEPTION_WINDOW_PADDING_MS,
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
if (startTime && endTime && startTime.getTime() > endTime.getTime()) {
|
|
5784
|
+
return Response.sendErrorResponse(
|
|
5785
|
+
req,
|
|
5786
|
+
res,
|
|
5787
|
+
new BadDataException("startTime must not be after endTime"),
|
|
5788
|
+
);
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
/* Pin to the session the caller already knows threw, when it does. */
|
|
5792
|
+
const pinnedSessionId: unknown = body["sessionId"];
|
|
4790
5793
|
|
|
4791
5794
|
const sessions: Array<SessionReplayExceptionSession> =
|
|
4792
5795
|
await SessionReplayReadService.getSessionsForException({
|
|
@@ -4795,10 +5798,13 @@ router.post(
|
|
|
4795
5798
|
accessibleRumApplicationIds: accessibleApplications.applicationIds,
|
|
4796
5799
|
...(startTime !== undefined && { startTime }),
|
|
4797
5800
|
...(endTime !== undefined && { endTime }),
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
5801
|
+
...(typeof pinnedSessionId === "string" &&
|
|
5802
|
+
pinnedSessionId.length > 0 && { sessionId: pinnedSessionId }),
|
|
5803
|
+
limit: readLimitFromBody(
|
|
5804
|
+
body,
|
|
5805
|
+
MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT,
|
|
5806
|
+
MAX_SESSION_REPLAY_FOR_EXCEPTION_LIMIT,
|
|
5807
|
+
),
|
|
4802
5808
|
});
|
|
4803
5809
|
|
|
4804
5810
|
return Response.sendJsonObjectResponse(req, res, {
|
|
@@ -4890,9 +5896,20 @@ router.post(
|
|
|
4890
5896
|
sessionReplayAllowedOrigins: true,
|
|
4891
5897
|
sessionReplaySamplePercentage: true,
|
|
4892
5898
|
sessionReplayCaptureTrigger: true,
|
|
5899
|
+
sessionReplayConsentMode: true,
|
|
5900
|
+
sessionReplayMaskingMode: true,
|
|
5901
|
+
sessionReplayRetentionInDays: true,
|
|
4893
5902
|
sessionReplayMonthlyBudgetInGB: true,
|
|
4894
5903
|
sessionReplayLastChunkReceivedAt: true,
|
|
4895
5904
|
sessionReplayBudgetExceededAt: true,
|
|
5905
|
+
/*
|
|
5906
|
+
* Stamped by the /config route on every recorder load (through
|
|
5907
|
+
* the throttled updateLastSeen path), so this is "when did the
|
|
5908
|
+
* recorder last run on the customer's site" - the fact that
|
|
5909
|
+
* separates "never installed" from "installed but uploading
|
|
5910
|
+
* nothing".
|
|
5911
|
+
*/
|
|
5912
|
+
lastSeenAt: true,
|
|
4896
5913
|
},
|
|
4897
5914
|
props: {
|
|
4898
5915
|
isRoot: true,
|
|
@@ -4919,15 +5936,51 @@ router.post(
|
|
|
4919
5936
|
},
|
|
4920
5937
|
});
|
|
4921
5938
|
|
|
4922
|
-
const
|
|
5939
|
+
const appIdentifier: string = String(
|
|
5940
|
+
(application as unknown as JSONObject)["appIdentifier"] || "",
|
|
5941
|
+
);
|
|
5942
|
+
|
|
5943
|
+
/*
|
|
5944
|
+
* Every counter below is independent and every one answers null on
|
|
5945
|
+
* its own failure: the Redis-backed ones when Redis is down, the
|
|
5946
|
+
* ClickHouse-backed summary when the query fails. null is rendered
|
|
5947
|
+
* as "unknown", never as 0 - "nothing was refused" and "we could
|
|
5948
|
+
* not count" are different diagnoses.
|
|
5949
|
+
*/
|
|
5950
|
+
const [
|
|
5951
|
+
projectBytesUsedToday,
|
|
5952
|
+
applicationBytesUsedThisMonth,
|
|
5953
|
+
refusalsLast24h,
|
|
5954
|
+
dropsLast24h,
|
|
5955
|
+
activity,
|
|
5956
|
+
]: [
|
|
4923
5957
|
number | null,
|
|
4924
5958
|
number | null,
|
|
5959
|
+
Array<SessionReplayRefusalCount> | null,
|
|
5960
|
+
Array<SessionReplayDropCount> | null,
|
|
5961
|
+
SessionReplayApplicationActivitySummary,
|
|
4925
5962
|
] = await Promise.all([
|
|
4926
5963
|
SessionReplayUsage.getProjectBytesUsedToday(projectId),
|
|
4927
5964
|
SessionReplayUsage.getApplicationBytesUsedThisMonth({
|
|
4928
5965
|
projectId: projectId,
|
|
4929
5966
|
rumApplicationId: rumApplicationId,
|
|
4930
5967
|
}),
|
|
5968
|
+
appIdentifier
|
|
5969
|
+
? SessionReplayHealthCounters.readRefusalsLast24h({
|
|
5970
|
+
projectId: projectId,
|
|
5971
|
+
appIdentifier: appIdentifier,
|
|
5972
|
+
})
|
|
5973
|
+
: Promise.resolve(null),
|
|
5974
|
+
appIdentifier
|
|
5975
|
+
? SessionReplayHealthCounters.readDropsLast24h({
|
|
5976
|
+
projectId: projectId,
|
|
5977
|
+
appIdentifier: appIdentifier,
|
|
5978
|
+
})
|
|
5979
|
+
: Promise.resolve(null),
|
|
5980
|
+
SessionReplayReadService.getApplicationActivitySummary({
|
|
5981
|
+
projectId: projectId,
|
|
5982
|
+
rumApplicationId: rumApplicationId,
|
|
5983
|
+
}),
|
|
4931
5984
|
]);
|
|
4932
5985
|
|
|
4933
5986
|
/*
|
|
@@ -4947,6 +6000,27 @@ router.post(
|
|
|
4947
6000
|
|
|
4948
6001
|
const applicationView: JSONObject = application as unknown as JSONObject;
|
|
4949
6002
|
|
|
6003
|
+
const toIsoOrNull: (value: unknown) => string | null = (
|
|
6004
|
+
value: unknown,
|
|
6005
|
+
): string | null => {
|
|
6006
|
+
if (value instanceof Date) {
|
|
6007
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
6008
|
+
}
|
|
6009
|
+
|
|
6010
|
+
if (typeof value === "string" && value.length > 0) {
|
|
6011
|
+
const parsed: number = Date.parse(value);
|
|
6012
|
+
|
|
6013
|
+
return Number.isFinite(parsed)
|
|
6014
|
+
? new Date(parsed).toISOString()
|
|
6015
|
+
: null;
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
return null;
|
|
6019
|
+
};
|
|
6020
|
+
|
|
6021
|
+
const retentionInDays: unknown =
|
|
6022
|
+
applicationView["sessionReplayRetentionInDays"];
|
|
6023
|
+
|
|
4950
6024
|
return Response.sendJsonObjectResponse(req, res, {
|
|
4951
6025
|
isProjectAllowed: Boolean(
|
|
4952
6026
|
(project as unknown as JSONObject | null)?.["isSessionReplayAllowed"],
|
|
@@ -4954,7 +6028,7 @@ router.post(
|
|
|
4954
6028
|
isApplicationEnabled: Boolean(
|
|
4955
6029
|
applicationView["isSessionReplayEnabled"],
|
|
4956
6030
|
),
|
|
4957
|
-
appIdentifier:
|
|
6031
|
+
appIdentifier: appIdentifier,
|
|
4958
6032
|
allowedOrigins: (applicationView["sessionReplayAllowedOrigins"] ||
|
|
4959
6033
|
[]) as JSONArray,
|
|
4960
6034
|
samplePercentage: Number(
|
|
@@ -4974,6 +6048,52 @@ router.post(
|
|
|
4974
6048
|
applicationBytesUsedThisMonth: applicationBytesUsedThisMonth,
|
|
4975
6049
|
monthlyBudgetInGB:
|
|
4976
6050
|
(applicationView["sessionReplayMonthlyBudgetInGB"] as number) ?? null,
|
|
6051
|
+
|
|
6052
|
+
/*
|
|
6053
|
+
* ---- Additive: the rest of RecordingHealthStatus. ----
|
|
6054
|
+
* Every timestamp is ISO-8601 or null; every counter is a number
|
|
6055
|
+
* or null (unknown). The Dashboard parses this with
|
|
6056
|
+
* parseRecordingHealthStatus and diagnoses it with
|
|
6057
|
+
* diagnoseRecordingHealth.
|
|
6058
|
+
*/
|
|
6059
|
+
consentMode: String(applicationView["sessionReplayConsentMode"] || ""),
|
|
6060
|
+
maskingMode: String(applicationView["sessionReplayMaskingMode"] || ""),
|
|
6061
|
+
retentionInDays:
|
|
6062
|
+
typeof retentionInDays === "number" &&
|
|
6063
|
+
Number.isFinite(retentionInDays)
|
|
6064
|
+
? retentionInDays
|
|
6065
|
+
: null,
|
|
6066
|
+
publishedRecorderVersion:
|
|
6067
|
+
SessionReplayReadService.getPublishedRecorderVersion(),
|
|
6068
|
+
lastConfigFetchAt: toIsoOrNull(applicationView["lastSeenAt"]),
|
|
6069
|
+
lastSessionStartedAt: activity.lastSessionStartedAt
|
|
6070
|
+
? activity.lastSessionStartedAt.toISOString()
|
|
6071
|
+
: null,
|
|
6072
|
+
sessionsLast24h: activity.sessionsLast24h,
|
|
6073
|
+
playableSessionsLast24h: activity.playableSessionsLast24h,
|
|
6074
|
+
/*
|
|
6075
|
+
* What the newest session's recorder said it could capture.
|
|
6076
|
+
*
|
|
6077
|
+
* This is the row the docs point operators at for spotting a stale
|
|
6078
|
+
* cached recorder artifact ("click labels: no"); without it the
|
|
6079
|
+
* health card and the installation test both said "not reported
|
|
6080
|
+
* yet" for every application forever, which reads as a bug rather
|
|
6081
|
+
* than as information. null (not []) when there is no session, when
|
|
6082
|
+
* the newest one predates the attribute, or when the query failed -
|
|
6083
|
+
* all three are "we cannot say", never "this recorder can do
|
|
6084
|
+
* nothing". It rides on the last-session query the summary already
|
|
6085
|
+
* runs, so the row costs no extra ClickHouse round trip.
|
|
6086
|
+
*/
|
|
6087
|
+
recorderCapabilities:
|
|
6088
|
+
activity.recorderCapabilities as unknown as JSONArray | null,
|
|
6089
|
+
refusalsLast24h: refusalsLast24h as unknown as JSONArray | null,
|
|
6090
|
+
/*
|
|
6091
|
+
* Kept apart from refusals: a refusal was answered to the
|
|
6092
|
+
* recorder, a drop happened after a 202 inside the worker. "12
|
|
6093
|
+
* chunks dropped after acceptance: scrub-incomplete" is a
|
|
6094
|
+
* different sentence from "212 uploads refused".
|
|
6095
|
+
*/
|
|
6096
|
+
dropsLast24h: dropsLast24h as unknown as JSONArray | null,
|
|
4977
6097
|
});
|
|
4978
6098
|
} catch (err: unknown) {
|
|
4979
6099
|
next(err);
|