Skip to content

Collect websocket metrics and allow filtering by endpoint family#13324

Merged
vdonato merged 2 commits intodevelopfrom
vdonato/new-websocket-metrics
Dec 16, 2025
Merged

Collect websocket metrics and allow filtering by endpoint family#13324
vdonato merged 2 commits intodevelopfrom
vdonato/new-websocket-metrics

Conversation

@vdonato
Copy link
Copy Markdown
Collaborator

@vdonato vdonato commented Dec 12, 2025

Describe your changes

In #13265, we began the process of extending our metrics collection to be more general than
just being able to collect cache stats.

In this PR, we do a few things to continue the work done in the previous PR:

  • continue to iterate on the interface to support requesting only specific metrics families.
    We need to do this because cache stats are naturally slow to compute (we should try to optimize
    this, but naturally it'll take time to serialize an object to compute its size), and some of the new
    metrics we'll be adding will be polled for frequently
  • Add new metrics to track websocket connects, disconnects, reconnects, and the number
    of active user sessions.
  • Make some changes to the metrics endpoint itself to allow the caller to request specific
    metrics families.

Testing Plan

  • Unit Tests (JS and/or Python)
    Python unit tests added
  • Any manual testing needed?
    Validated the new endpoint on a local dev environment via curl

@vdonato vdonato added security-assessment-completed impact:internal PR changes only affect internal code change:feature PR contains new feature or enhancement implementation labels Dec 12, 2025
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Dec 12, 2025

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 12, 2025

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-13324/streamlit-1.52.1-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-13324.streamlit.app (☁️ Deploy here if not accessible)

@vdonato vdonato force-pushed the vdonato/new-websocket-metrics branch 2 times, most recently from b6f1ed0 to f7dd6e3 Compare December 13, 2025 00:12
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 13, 2025

📉 Python coverage change detected

The Python unit test coverage has decreased by 0.0493%

  • Current PR: 92.7958% (20960 statements, 1510 missed)
  • Latest develop: 92.8451% (20825 statements, 1490 missed)

💡 Consider adding more unit tests to maintain or improve coverage.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 142 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 100 25 75%
streamlit/cli_util.py 39 6 85%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 11 80%
streamlit/commands/execution_control.py 70 10 86%
streamlit/commands/experimental_query_params.py 40 2 95%
streamlit/commands/logo.py 41 6 85%
streamlit/commands/navigation.py 106 2 98%
streamlit/commands/page_config.py 106 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 49 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 8 76%
streamlit/components/v1/component_registry.py 41 3 93%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 85 7 92%
streamlit/components/v2/__init__.py 24 0 100%
streamlit/components/v2/bidi_component/__init__.py 4 0 100%
streamlit/components/v2/bidi_component/constants.py 5 0 100%
streamlit/components/v2/bidi_component/main.py 152 17 89%
streamlit/components/v2/bidi_component/serialization.py 81 2 98%
streamlit/components/v2/bidi_component/state.py 13 0 100%
streamlit/components/v2/component_definition_resolver.py 30 0 100%
streamlit/components/v2/component_file_watcher.py 117 9 92%
streamlit/components/v2/component_manager.py 97 13 87%
streamlit/components/v2/component_manifest_handler.py 24 0 100%
streamlit/components/v2/component_path_utils.py 68 5 93%
streamlit/components/v2/component_registry.py 121 8 93%
streamlit/components/v2/get_bidi_component_manager.py 8 1 88%
streamlit/components/v2/manifest_scanner.py 224 25 89%
streamlit/components/v2/presentation.py 84 19 77%
streamlit/components/v2/types.py 8 8 0%
streamlit/config.py 412 12 97%
streamlit/config_option.py 79 3 96%
streamlit/config_util.py 288 7 98%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 45 0 100%
streamlit/connections/snowflake_connection.py 60 13 78%
streamlit/connections/snowpark_connection.py 44 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 130 1 99%
streamlit/dataframe_util.py 501 47 91%
streamlit/delta_generator.py 250 7 97%
streamlit/delta_generator_singletons.py 74 7 91%
streamlit/deprecation_util.py 59 4 93%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 60 0 100%
streamlit/elements/arrow.py 201 15 93%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 9 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 104 10 90%
streamlit/elements/dialog_decorator.py 38 0 100%
streamlit/elements/doc_string.py 227 9 96%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 56 2 96%
streamlit/elements/graphviz_chart.py 35 1 97%
streamlit/elements/heading.py 56 0 100%
streamlit/elements/html.py 49 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 32 0 100%
streamlit/elements/json.py 39 2 95%
streamlit/elements/layouts.py 140 3 98%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 390 26 93%
streamlit/elements/lib/color_util.py 100 4 96%
streamlit/elements/lib/column_config_utils.py 169 1 99%
streamlit/elements/lib/column_types.py 189 4 98%
streamlit/elements/lib/dialog.py 69 1 99%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 30 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 176 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 121 1 99%
streamlit/elements/lib/mutable_status_container.py 73 4 95%
streamlit/elements/lib/options_selector_utils.py 90 0 100%
streamlit/elements/lib/pandas_styler_utils.py 80 2 98%
streamlit/elements/lib/policies.py 56 1 98%
streamlit/elements/lib/shortcut_utils.py 42 2 95%
streamlit/elements/lib/streamlit_plotly_theme.py 49 0 100%
streamlit/elements/lib/subtitle_utils.py 76 13 83%
streamlit/elements/lib/utils.py 76 5 93%
streamlit/elements/map.py 110 1 99%
streamlit/elements/markdown.py 65 2 97%
streamlit/elements/media.py 181 8 96%
streamlit/elements/metric.py 104 0 100%
streamlit/elements/pdf.py 49 2 96%
streamlit/elements/plotly_chart.py 129 6 95%
streamlit/elements/progress.py 36 0 100%
streamlit/elements/pyplot.py 39 2 95%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/space.py 12 0 100%
streamlit/elements/spinner.py 44 3 93%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 228 3 99%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 68 10 85%
streamlit/elements/widgets/button.py 245 6 98%
streamlit/elements/widgets/button_group.py 171 1 99%
streamlit/elements/widgets/camera_input.py 62 10 84%
streamlit/elements/widgets/chat.py 237 58 76%
streamlit/elements/widgets/checkbox.py 52 0 100%
streamlit/elements/widgets/color_picker.py 59 2 97%
streamlit/elements/widgets/data_editor.py 254 14 94%
streamlit/elements/widgets/file_uploader.py 108 18 83%
streamlit/elements/widgets/multiselect.py 105 4 96%
streamlit/elements/widgets/number_input.py 143 5 97%
streamlit/elements/widgets/radio.py 83 5 94%
streamlit/elements/widgets/select_slider.py 97 0 100%
streamlit/elements/widgets/selectbox.py 91 2 98%
streamlit/elements/widgets/slider.py 241 8 97%
streamlit/elements/widgets/text_widgets.py 130 6 95%
streamlit/elements/widgets/time_widgets.py 390 19 95%
streamlit/elements/write.py 166 32 81%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 194 25 87%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 141 82 42%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 5 95%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 78 2 97%
streamlit/net_util.py 55 3 95%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 460 91 80%
streamlit/runtime/caching/__init__.py 19 0 100%
streamlit/runtime/caching/cache_data_api.py 171 3 98%
streamlit/runtime/caching/cache_errors.py 45 4 91%
streamlit/runtime/caching/cache_resource_api.py 132 1 99%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 165 9 95%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 311 25 92%
streamlit/runtime/caching/legacy_cache_api.py 14 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 31 2 94%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 67 1 99%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/connection_factory.py 85 9 89%
streamlit/runtime/context.py 140 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/download_data_util.py 27 0 100%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 112 2 98%
streamlit/runtime/media_file_manager.py 110 7 94%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 73 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 46 1 98%
streamlit/runtime/metrics_util.py 193 12 94%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 251 18 93%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 230 27 88%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 11 1 91%
streamlit/runtime/scriptrunner_utils/script_requests.py 106 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 134 2 99%
streamlit/runtime/secrets.py 242 25 90%
streamlit/runtime/session_manager.py 60 1 98%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 52 2 96%
streamlit/runtime/state/presentation.py 19 4 79%
streamlit/runtime/state/query_params.py 134 5 96%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 9 88%
streamlit/runtime/state/session_state.py 440 33 92%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 15 1 93%
streamlit/runtime/stats.py 138 21 85%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 3 92%
streamlit/runtime/websocket_session_manager.py 97 0 100%
streamlit/source_util.py 36 1 97%
streamlit/string_util.py 92 8 91%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 242 6 98%
streamlit/testing/v1/element_tree.py 1371 87 94%
streamlit/testing/v1/local_script_runner.py 71 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 28 1 96%
streamlit/type_util.py 138 12 91%
streamlit/url_util.py 39 5 87%
streamlit/user_info.py 87 8 91%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 181 24 87%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 42 3 93%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 49 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 153 20 87%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 186 17 91%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_static_file_handler.py 29 3 90%
streamlit/web/server/authlib_tornado_integration.py 42 5 88%
streamlit/web/server/bidi_component_request_handler.py 65 8 88%
streamlit/web/server/browser_websocket_handler.py 115 31 73%
streamlit/web/server/component_file_utils.py 24 0 100%
streamlit/web/server/component_request_handler.py 55 4 93%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 118 18 85%
streamlit/web/server/oidc_mixin.py 46 0 100%
streamlit/web/server/routes.py 90 7 92%
streamlit/web/server/server.py 188 11 94%
streamlit/web/server/server_util.py 67 5 93%
streamlit/web/server/stats_request_handler.py 59 5 92%
streamlit/web/server/upload_file_request_handler.py 59 14 76%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 20960 1510 93%

📊 View detailed coverage comparison

@vdonato vdonato force-pushed the vdonato/new-websocket-metrics branch from f7dd6e3 to 16a014e Compare December 13, 2025 01:35
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 13, 2025

📉 Frontend coverage change detected

The frontend unit test (vitest) coverage has decreased by 0.0000%

  • Current PR: 86.3400% (12614 lines, 1722 missed)
  • Latest develop: 86.3400% (12614 lines, 1722 missed)

✅ Coverage change is within normal range.

📊 View detailed coverage comparison

@sfc-gh-vdonato sfc-gh-vdonato marked this pull request as ready for review December 13, 2025 07:03
@sfc-gh-vdonato sfc-gh-vdonato requested a review from a team as a code owner December 13, 2025 07:03
Copilot AI review requested due to automatic review settings December 13, 2025 07:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the metrics collection system to support filtering metrics by family name and adds new websocket session metrics (connections, reconnections, disconnections, and active sessions count). The filtering capability allows callers to request only specific metrics families, avoiding the performance cost of computing expensive cache statistics when only lightweight metrics are needed.

Key Changes

  • Modified StatsProvider protocol and StatsManager to support optional filtering by metric family names
  • Added CounterStat and GaugeStat classes for tracking session events and active session counts
  • Implemented metrics collection in WebsocketSessionManager to track connect, reconnect, and disconnect events
  • Updated the metrics endpoint to accept families query parameters for filtering

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
lib/streamlit/runtime/stats.py Added CounterStat and GaugeStat classes; modified StatsProvider protocol and StatsManager to support family filtering; added family name constants
lib/streamlit/runtime/websocket_session_manager.py Implemented StatsProvider protocol with event counters; added metrics tracking for session lifecycle events
lib/streamlit/web/server/stats_request_handler.py Added query parameter parsing for family filtering; updated to pass filtered families to stats manager
lib/streamlit/runtime/runtime.py Updated provider registration to use new API; registered session manager for session metrics families
lib/streamlit/web/server/server.py Updated media file storage provider registration to use new API
lib/streamlit/runtime/state/session_state.py Updated SessionState and SessionStateStatProvider to support family filtering
lib/streamlit/runtime/memory_uploaded_file_manager.py Updated get_stats to support family filtering and return dict format
lib/streamlit/runtime/memory_media_file_storage.py Updated get_stats to support family filtering and return dict format
lib/streamlit/runtime/caching/cache_data_api.py Updated cache providers to support family filtering in get_stats method
lib/streamlit/runtime/caching/cache_resource_api.py Updated cache providers to support family filtering in get_stats method
lib/streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py Updated storage wrapper to support family filtering
lib/tests/streamlit/runtime/stats_test.py Added comprehensive tests for filtering, CounterStat, GaugeStat, and updated mock provider
lib/tests/streamlit/runtime/websocket_session_manager_test.py Added test suite for websocket session metrics tracking
lib/tests/streamlit/web/server/stats_handler_test.py Added tests for HTTP endpoint filtering and new stat formats
lib/tests/streamlit/runtime/state/session_state_test.py Updated tests to work with new dict-based return format
lib/tests/streamlit/runtime/memory_media_file_storage_test.py Updated tests to work with new dict-based return format
lib/tests/streamlit/runtime/uploaded_file_manager_test.py Updated tests to work with new dict-based return format
lib/tests/streamlit/runtime/caching/cache_data_api_test.py Updated tests to work with new dict-based return format
lib/tests/streamlit/runtime/caching/cache_resource_api_test.py Updated tests to work with new dict-based return format

@vdonato vdonato force-pushed the vdonato/new-websocket-metrics branch 2 times, most recently from 79b0028 to f8ca4f2 Compare December 13, 2025 22:20
@lukasmasuch
Copy link
Copy Markdown
Collaborator

@cursor review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no bugs!


"""
result: dict[str, list[Stat]] = {}

if family_names is None or SESSION_EVENTS_FAMILY in family_names:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a metric for "user time spent in session" or similar? Looking for some way to count the number of seconds that users have had an active session in an app.

I realize this is better suited to an event bus rather than reported as a metric, but this is a quick and dirty way to get this info. Keeping it as a monotonic counter incremented when a session ends would be amazing.

cc @sfc-gh-trichards

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do -- I'll probably defer this until ~Wednesday or so to get this PR merged first so that it doesn't continue to grow, but adding another counter for this should be pretty straightforward.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a single aggregate counter tracking the total number of seconds that all users have spent in aggregate in the app be sufficient here, or are we looking for more granularity?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a quick followup to add a counter metric for this: #13414

@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Dec 15, 2025
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Dec 15, 2025
@vdonato vdonato added the ai-review If applied to PR or issue will run AI review workflow label Dec 15, 2025
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Dec 15, 2025
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR extends Streamlit's metrics collection capabilities by:

  1. Adding new WebSocket session metrics (connection, reconnection, disconnection counters, and active sessions gauge)
  2. Implementing metric family filtering to allow requesting specific metrics via the families query parameter
  3. Introducing new stat types (CounterStat and GaugeStat) alongside the existing CacheStat
  4. Updating the StatsProvider protocol to support filtered stat retrieval

This is a continuation of work from #13265 to make the metrics system more general and performant, as cache stats are expensive to compute and new metrics may need frequent polling.

Code Quality

The code is well-structured and follows existing patterns in the codebase. Key observations:

Positives:

  • Clean, well-documented code with proper docstrings (Numpy style)
  • Comprehensive type annotations throughout
  • Thread-safe implementation using _stats_lock in WebsocketSessionManager for counter access
  • Good separation of concerns with the Stat protocol for polymorphic stat handling
  • Proper use of Final constants for metric family names

Minor observations:

  1. Protocol stub convention (lib/streamlit/runtime/stats.py:44-51): The Stat protocol uses pass for method stubs. While this works, the conventional way is to use ... (ellipsis):
@property
def family_name(self) -> str:
    ...  # More conventional for protocols
  1. Lock consistency for active_sessions (lib/streamlit/runtime/websocket_session_manager.py:255): The active_sessions metric reads len(self._active_session_info_by_id) without holding a lock, while counter reads use _stats_lock. While reading dict length is atomic in CPython, this inconsistency could be a concern if the implementation changes:
if family_names is None or ACTIVE_SESSIONS_FAMILY in family_names:
    result[ACTIVE_SESSIONS_FAMILY] = [
        GaugeStat(
            family_name=ACTIVE_SESSIONS_FAMILY,
            value=len(self._active_session_info_by_id),  # No lock held here
            help="Current number of active sessions.",
        ),
    ]
  1. TODO acknowledged (lib/streamlit/runtime/stats.py:72-74): There's an existing TODO about potentially consolidating CacheStat with GaugeStat. This is reasonable technical debt for future cleanup.

Test Coverage

Excellent test coverage overall:

  • websocket_session_manager_test.py: Comprehensive tests for all session metrics scenarios:

    • Initial stats verification (all zeros)
    • Connection, reconnection, disconnection counter increments
    • Active sessions gauge accuracy
    • Edge cases (invalid session IDs, already-connected sessions)
    • Correct stat format verification
  • stats_test.py: Thorough tests for:

    • StatsManager filtering behavior
    • Multi-family provider handling
    • CounterStat and GaugeStat protocol compliance
    • Metric string formatting with/without labels
    • metric_type_string_to_proto mapping
  • stats_handler_test.py: HTTP endpoint tests for:

    • Family filtering via query parameters
    • Multiple family filtering
    • Counter and gauge text format output
  • Other test files updated: Cache and storage tests properly updated to reflect the new return type.

All tests follow pytest best practices with descriptive docstrings.

Backwards Compatibility

⚠️ Breaking change to StatsProvider interface:

The StatsProvider.get_stats() method signature changed from:

def get_stats(self) -> list[CacheStat]

to:

def get_stats(self, family_names: Sequence[str] | None = None) -> Mapping[str, Sequence[Stat]]

This affects:

  • All internal StatsProvider implementations (updated in this PR)
  • Any external implementations (if any exist)

Mitigating factors:

  • StatsProvider is defined as a Protocol, so external code using duck-typing would need updates
  • This appears to be an internal API not documented for external use
  • The metrics endpoint behavior is backwards compatible (no filter = all metrics)

Security & Risk

No security concerns identified:

  • Metrics endpoint doesn't expose sensitive information
  • Thread safety is properly handled for counters
  • No new authentication/authorization requirements

Low regression risk:

  • Changes are additive (new metrics)
  • Existing cache stats functionality preserved
  • All internal implementations updated consistently

Recommendations

  1. Consider documentation update: If StatsProvider is intended for external use, document the API change in release notes or migration guide.

  2. Optional lock consistency (low priority): Consider whether the active sessions read should also be protected or if a comment explaining why it's safe without a lock would be helpful:

# Reading dict length is atomic in CPython; no lock needed
value=len(self._active_session_info_by_id),
  1. Future consideration: The TODO about consolidating CacheStat with GaugeStat could simplify the codebase. Consider creating a follow-up issue to track this.

Verdict

APPROVED: This is a well-implemented feature that extends Streamlit's metrics collection with proper test coverage. The breaking change to StatsProvider is acceptable given it's an internal API, and all internal implementations have been properly updated. The code follows existing patterns and best practices.


This is an automated AI review. Please verify the feedback and use your judgment.

@streamlit streamlit deleted a comment from github-actions bot Dec 15, 2025
Comment on lines +945 to +946
if family_names is not None and CACHE_MEMORY_FAMILY not in family_names:
return {}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why do we need this extra check in every get_stats. Since the cache provider is already registered with a family, wouldn't it be possible to simply not call get_stats of the provider?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this must be left over from when I was still going through some ideas of how to best express that we may serve various stat families from the same StatsProvider.

Let me replace it with a comment saying that we don't need to bother doing any other checks on which stats to serve if this get_stats (and those that are essentially identical) since we only serve one stats family from it.

self._stats_mgr = StatsManager()
self._stats_mgr.register_provider(
"cache_memory_bytes", get_data_cache_stats_provider()
get_data_cache_stats_provider(), [CACHE_MEMORY_FAMILY]
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Would it be possible to just add a property/method to the provider that returns the supported families? I assume a provider always returns the same families, and this would allow self._stats_mgr.register_provider(get_data_cache_stats_provider()) and potentially make it slightly more maintainable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, this should be possible. Let me tweak the StatsProvider interface to include this info.

Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, LGMT 👍 However, the context of the Starlette migration, I'm a bit nervous that some aspects will get lost/break once we make Starlette the default without e2e coverage. I think it might be possible to add some basic e2e tests to ensure that, e.g. filtering is supported and some counting happens on websocket disconnects / reconnects.

Related tests: web_server -> Contains some very basic metric endpoint e2e tests already. websocket_reconnects, websocket_disconnect

@vdonato vdonato force-pushed the vdonato/new-websocket-metrics branch from f8ca4f2 to 89a9876 Compare December 16, 2025 21:44
@vdonato vdonato force-pushed the vdonato/new-websocket-metrics branch from 89a9876 to 72aa2d6 Compare December 16, 2025 22:01
@vdonato
Copy link
Copy Markdown
Collaborator Author

vdonato commented Dec 16, 2025

I think it might be possible to add some basic e2e tests to ensure that, e.g. filtering is supported and some counting happens on websocket disconnects / reconnects.

I added some e2e tests to verify that filtering works. Didn't add tests to also consider websocket connect/disconnect increments the metric counts, though, since that code is already pretty well covered by tests of the websocket session manager, which the starlette migration uses without any changes. I'll probably still look to add some e2e test coverage here in a followup (I have a few that I'll get around to late this week) but don't want to increase the size of this PR any further.

@vdonato vdonato merged commit 4d57a44 into develop Dec 16, 2025
43 checks passed
@vdonato vdonato deleted the vdonato/new-websocket-metrics branch December 16, 2025 22:24
vdonato added a commit that referenced this pull request Dec 18, 2025
In #13324, we added e2e tests to validate that session event metrics were being served but didn't add
tests on the metrics behavior itself because:
   * there's already decent test coverage around this in websocket session
     manager unit tests.
   * that PR was already getting much longer than initially expected.

This PR goes back to add Playwright tests to validate that session event counter metrics increase
when we expect them to.
vdonato added a commit that referenced this pull request Jan 5, 2026
While this isn't exactly the best way of doing this, it'd be useful for us to have a new metric to track
the total length of time users spend in active sessions (see #13324 (comment)).

This PR adds this new metric by keeping track of session connect/reconnect times and incrementing
a simple accumulator for aggregate session length each time a session is either disconnected or
closed.

Note that one caveat of doing this is that long-lived active sessions won't be counted until disconnected
or closed, but we're not too worried our data being slow to update due to this.
majiayu000 pushed a commit to majiayu000/streamlit that referenced this pull request Jan 9, 2026
While this isn't exactly the best way of doing this, it'd be useful for us to have a new metric to track
the total length of time users spend in active sessions (see streamlit#13324 (comment)).

This PR adds this new metric by keeping track of session connect/reconnect times and incrementing
a simple accumulator for aggregate session length each time a session is either disconnected or
closed.

Note that one caveat of doing this is that long-lived active sessions won't be counted until disconnected
or closed, but we're not too worried our data being slow to update due to this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:internal PR changes only affect internal code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants