Skip to content

Add opt-in community usage analytics - #791

Merged
n00bcodr merged 13 commits into
mainfrom
analytics
Aug 30, 2026
Merged

Add opt-in community usage analytics#791
n00bcodr merged 13 commits into
mainfrom
analytics

Conversation

@n00bcodr

@n00bcodr n00bcodr commented Aug 28, 2026

Copy link
Copy Markdown
Owner

This adds a new "Usage Statistics" section under the Admin tab. Off by default, and nothing is sent anywhere unless an admin explicitly turns it on and saves. #788


When enabled, the plugin periodically sends a small anonymous report to a new community analytics project. There are three independently toggleable categories:

  • Feature toggle states - on/off for every boolean setting, would be applicable for all existing on/off toggles and the ones that might be added going forward.
    • A small hand-picked set of non-boolean settings (icon style, tag positions, etc.) is included too, but nothing that could ever be a URL or key.
  • Feature usage counts - a couple of action counters (e.g. Seerr requests submitted) plus point-in-time totals for bookmarks, hidden-content items, spoiler-blur items, and reviews, each paired with how many installs actually have any (not just the raw count).
  • Data file sizes - just the custom branding folder's byte size, never contents - this is to give visibility if anyone is using custom branding.

There's a Preview button on the config page that shows the exact JSON that would be sent.


Along with this, there is a new live dashboard page in the docs, Community Analytics, that reads straight from the db. This will be visible live at - https://n00bcodr.github.io/Jellyfin-/advanced/community-analytics/ once the PR is merged.

image image

Sample JSON that is sent

{
  "InstallId": "<randomly_generated_unique_identifier>",
  "PluginVersion": "12.4.1",
  "JellyfinVersion": "12.0.0",
  "JellyfinTarget": "jf12",
  "Config": {
    "MaintenanceModeEnabled": false,
    "EnableCustomSplashScreen": false,
    "DevMode": false,
    "ElsewhereEnabled": true,
    "AutoPauseEnabled": true,
    "AutoResumeEnabled": false,
    "AutoPipEnabled": false,
    "AutoSkipIntro": false,
    "AutoSkipOutro": false,
    "LongPress2xEnabled": false,
    "RandomButtonEnabled": true,
    "RandomIncludeMovies": true,
    "RandomIncludeShows": true,
    "RandomUnwatchedOnly": false,
    "ShowWatchProgress": true,
    "ShowFileSizes": true,
    "RemoveContinueWatchingEnabled": true,
    "ShowAudioLanguages": true,
    "ShowReviews": true,
    "ShowUserReviews": true,
    "ReviewsExpandedByDefault": false,
    "HideReviewsFromHiddenUsers": true,
    "HideReviewsFromDisabledUsers": true,
    "ShowAwards": true,
    "MdblistRatingsEnabled": false,
    "MdblistRatingsShowOnItemDetails": true,
    "MdblistRatingsFetchEnabled": false,
    "MdblistRatingsAutoSyncEnabled": false,
    "MdblistRatingsOverwriteExisting": false,
    "MdblistRatingsShowPercentSymbol": false,
    "ShowReleaseDates": true,
    "ShowUserRatingOnPosters": false,
    "ShowUserRatingDash": true,
    "PauseScreenEnabled": true,
    "QualityTagsEnabled": true,
    "ShowResolutionTag": true,
    "ShowSourceTag": true,
    "ShowDynamicRangeTag": true,
    "ShowSpecialFormatTag": true,
    "ShowVideoCodecTag": true,
    "ShowAudioInfoTag": true,
    "LanguageTagsEnabled": true,
    "RatingTagsEnabled": true,
    "PeopleTagsEnabled": true,
    "DisableTagsOnSearchPage": false,
    "TagsHideOnHover": false,
    "TagCacheServerMode": true,
    "EnableTagsLocalStorageFallback": false,
    "DisableAllShortcuts": false,
    "DisableCustomSubtitleStyles": false,
    "LanguageTagsPriorityStrict": false,
    "ShowRatingInPlayer": true,
    "GenreTagsEnabled": true,
    "JellyseerrEnabled": true,
    "JellyseerrShowSearchResults": true,
    "JellyseerrShowReportButton": false,
    "JellyseerrShowIssueIndicator": false,
    "JellyseerrEnable4KRequests": false,
    "JellyseerrEnable4KTvRequests": false,
    "JellyseerrShowAdvanced": false,
    "JellyseerrShowQuotaInfo": true,
    "JellyseerrShowSimilar": true,
    "JellyseerrShowRecommended": true,
    "JellyseerrShowRequestMoreOnSeries": true,
    "JellyseerrShowNetworkDiscovery": true,
    "JellyseerrShowGenreDiscovery": true,
    "JellyseerrShowTagDiscovery": true,
    "JellyseerrShowPersonDiscovery": true,
    "JellyseerrShowCollectionDiscovery": true,
    "JellyseerrShowDetailPageLink": true,
    "JellyseerrShowDetailPageLinkAsText": false,
    "JellyseerrExcludeLibraryItems": true,
    "JellyseerrExcludeBlocklistedItems": false,
    "ShowElsewhereOnJellyseerr": false,
    "JellyseerrUseMoreInfoModal": true,
    "JellyseerrAvailablePosterLinksToJellyfin": false,
    "TriggerSeerrScanOnItemAdded": false,
    "ShowCollectionsInSearch": true,
    "JellyseerrDisableCache": false,
    "ArrLinksEnabled": false,
    "ShowArrLinksAsText": false,
    "ArrLinksShowStatusSingle": false,
    "ArrTagsSyncEnabled": false,
    "ArrTagsClearOldTags": true,
    "ArrTagsShowAsLinks": true,
    "LetterboxdEnabled": true,
    "ShowLetterboxdLinkAsText": false,
    "MetadataIconsEnabled": false,
    "AutoSeasonRequestEnabled": false,
    "AutoSeasonRequestRequireAllWatched": false,
    "AutoMovieRequestEnabled": false,
    "AutoMovieRequestCheckReleaseDate": true,
    "AutoMovieRequestFallbackOn4k": true,
    "AddRequestedMediaToWatchlist": false,
    "SyncJellyseerrWatchlist": false,
    "SyncJellyfinWatchlistToSeerr": false,
    "PreventWatchlistReAddition": true,
    "JellyseerrAutoImportUsers": false,
    "BookmarksEnabled": true,
    "BookmarksUsePluginPages": false,
    "BookmarksUseCustomTabs": false,
    "BookmarksUseNativeTab": true,
    "BookmarksAutoCreateCustomTab": false,
    "UseIcons": true,
    "ColoredRatingsEnabled": true,
    "ThemeSelectorEnabled": false,
    "ColoredActivityIconsEnabled": true,
    "PluginIconsEnabled": true,
    "EnableLoginImage": true,
    "ActiveStreamsEnabled": true,
    "ActiveStreamsAllUsers": false,
    "ActivityFeedEnabled": false,
    "ActivityFeedShowWatched": true,
    "ActivityFeedShowFavorited": true,
    "ActivityFeedShowReviewed": true,
    "ActivityFeedShowActiveStreams": true,
    "ActivityFeedUsePluginPages": false,
    "ActivityFeedUseCustomTabs": false,
    "ActivityFeedUseNativeTab": false,
    "ActivityFeedAutoCreateCustomTab": false,
    "DownloadsPageEnabled": true,
    "DownloadsUsePluginPages": false,
    "DownloadsUseCustomTabs": false,
    "DownloadsUseNativeTab": true,
    "DownloadsAutoCreateCustomTab": false,
    "DownloadsPagePollingEnabled": true,
    "DownloadsPageShowIssues": false,
    "ShowDownloadsInRequests": true,
    "DownloadsFilterByUserRequests": true,
    "DownloadsShowHistory": true,
    "DownloadsHistoryAdminOnly": false,
    "CalendarPageEnabled": true,
    "CalendarUseCustomTabs": false,
    "CalendarUsePluginPages": false,
    "CalendarUseNativeTab": true,
    "CalendarAutoCreateCustomTab": false,
    "CalendarHighlightFavorites": false,
    "CalendarHighlightWatchedSeries": false,
    "CalendarFilterByLibraryAccess": true,
    "CalendarShowOnlyRequested": false,
    "CalendarForceOnlyRequested": false,
    "RecommendationsPageEnabled": true,
    "RecommendationsUsePluginPages": false,
    "RecommendationsUseCustomTabs": false,
    "RecommendationsUseNativeTab": true,
    "RecommendationsAutoCreateCustomTab": false,
    "HiddenContentEnabled": true,
    "HiddenContentUsePluginPages": false,
    "HiddenContentUseCustomTabs": false,
    "HiddenContentUseNativeTab": true,
    "HiddenContentAutoCreateCustomTab": false,
    "HiddenContentAdmin": true,
    "HiddenContentDefaultEnabled": true,
    "HiddenContentDefaultShowHideButtons": true,
    "HiddenContentDefaultShowHideConfirmation": true,
    "HiddenContentDefaultShowButtonJellyseerr": true,
    "HiddenContentDefaultShowButtonLibrary": false,
    "HiddenContentDefaultShowButtonDetails": true,
    "HiddenContentDefaultShowButtonCast": false,
    "HiddenContentDefaultFilterLibrary": true,
    "HiddenContentDefaultFilterDiscovery": true,
    "HiddenContentDefaultFilterSearch": false,
    "HiddenContentDefaultFilterCalendar": true,
    "HiddenContentDefaultFilterUpcoming": true,
    "HiddenContentDefaultFilterRecommendations": true,
    "HiddenContentDefaultFilterRequests": true,
    "HiddenContentDefaultFilterNextUp": true,
    "HiddenContentDefaultFilterContinueWatching": true,
    "HiddenContentDefaultExperimentalHideCollections": false,
    "SpoilerBlurEnabled": false,
    "SpoilerStripOverview": true,
    "SpoilerStripTags": true,
    "SpoilerStripChapters": true,
    "SpoilerStripTaglines": true,
    "SpoilerStripRatings": true,
    "SpoilerStripPremiereDate": true,
    "SpoilerReplaceTitle": true,
    "SpoilerStripCast": true,
    "SpoilerStripReviews": true,
    "SpoilerBlurArtwork": false,
    "SpoilerAutoEnableOnFirstPlay": false,
    "SpoilerAutoEnableOnSeerrRequest": false,
    "SpoilerBlurStrictRefresh": false,
    "SpoilerKeepMoviePosters": true,
    "SpoilerAdvancedMode": false,
    "SpoilerNextEpisodeStripTitle": false,
    "SpoilerNextEpisodeStripOverview": true,
    "SpoilerNextEpisodeStripRatings": true,
    "SpoilerCurrentSeasonStripTitle": true,
    "SpoilerCurrentSeasonStripOverview": true,
    "SpoilerCurrentSeasonStripRatings": true,
    "TmdbEnabled": true,
    "MdblistEnabled": false
  },
  "Settings": {
    "MaintenanceModeAction": "none",
    "MaintenanceModeAffectedUsers": "all",
    "QualityTagsPosition": "top-left",
    "GenreTagsPosition": "top-right",
    "LanguageTagsPosition": "bottom-left",
    "RatingTagsPosition": "bottom-right",
    "IconStyle": "emoji"
  },
  "DataFileSizes": {
    "custom_branding": 36733
  },
  "Period": "2026-08-28",
  "Events": [
    {
      "key": "seerr.request_submitted",
      "count": 1
    },
    {
      "key": "total.bookmarks",
      "count": 2
    },
    {
      "key": "total.hidden_content_items",
      "count": 1
    },
    {
      "key": "total.spoiler_blur_items",
      "count": 0
    },
    {
      "key": "total.reviews",
      "count": 2
    }
  ]
}

Adds a "Usage Statistics" section to the Admin tab that, if an admin turns
it on, periodically reports a small anonymous snapshot to a new community
analytics project: which features are toggled on, a handful of usage
counts (bookmarks, reviews, hidden content, spoiler blur items, Seerr
requests), and byte sizes of a couple of data files. Off by default, three
independently toggleable categories, and a live preview button that shows
the exact payload before anything is ever sent.

Each install gets a random ID minted by the backend along with a secret
that has to be presented on every report, so nobody can overwrite or
impersonate another install's data just by guessing or copying an ID
that shows up in a shared preview. Opting back out flags the row instead
of deleting it, and a couple of self-heal paths handle the backend
rejecting stale credentials (e.g. after a manual cleanup) without the
admin having to do anything.

Results are visible on a new live dashboard page in the docs
(Community Analytics), reading straight from a few public aggregate-only
Supabase views. Feature-toggle grouping and display labels on that page
are generated from configPage.html's own fieldset/legend/label structure
(scripts/generate_config_flag_groups.py, run in CI before every docs
build), so a new setting is grouped and labeled correctly the moment
it's added to the right part of the config page, nothing to hand-maintain.

Also swept in along the way: two settings that were missing from
public-config (JellyseerrShowCollectionDiscovery silently never disabled,
peopletags.js reading a config key that didn't exist) and a matching
Postgres RPC overloading gotcha (fixed by keeping one function with a
defaulted param instead of two overloads).
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Comment thread Jellyfin.Plugin.JellyfinEnhanced/Services/UsageEventCounterService.cs Dismissed
Comment thread Jellyfin.Plugin.JellyfinEnhanced/Services/UsageEventCounterService.cs Dismissed
Comment thread Jellyfin.Plugin.JellyfinEnhanced/Services/AnalyticsReportingService.cs Dismissed
Comment thread Jellyfin.Plugin.JellyfinEnhanced/Services/UsageEventCounterService.cs Dismissed
Comment thread Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.cs Fixed
Comment thread Jellyfin.Plugin.JellyfinEnhanced/JellyfinEnhanced.cs Fixed

@4eh5xitv6787h645ebv 4eh5xitv6787h645ebv left a comment

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.

Leaving a commit to fix a few things, but is all otherwise looks good 👍

4eh5xitv6787h645ebv and others added 2 commits August 30, 2026 23:42
Review + live-test pass over the Usage Statistics feature (#791, discussion
capability removed; everything verified against a Jellyfin 10.11 container
with the analytics backend blackholed.

Privacy
- Stop sending raw Jellyfin user GUIDs: MaintenanceModeAffectedUsers is no
  longer [AnalyticsInclude]; the payload and the anonymous public-config
  endpoint share one derived "all"/"selected" helper so the two shapes can
  never drift or regress to the raw value.
- Normalize LanguageTagsPriority (a free-text box) to language-code tokens
  only (up to two BCP-47 subtags, so zh-Hans-CN survives).
- Gate the server-side counter Increment on consent so no pre-opt-in
  activity is accumulated; clear counters synchronously at the opt-in
  boundary (in UpdateConfiguration, before anything can act on the newly
  enabled state) so no send can ever ship pre-consent data.

Abuse / integrity
- usage/track accepts only allowlisted keys, blocking unbounded distinct-key
  spam and forged "total.*" values; rejections of well-formed unknown keys
  log loudly (a new counter shipped without its KnownKeys entry would
  otherwise read as "unused" forever); persisted counters from pre-allowlist
  builds are filtered on load.
- Preserve server-minted install credentials when an incoming save has
  EITHER half missing (covers secret-redacted backup restores), write them
  to the live config under a shared lock covering UpdateConfiguration's
  object swap, and log post-success SaveConfiguration failures honestly
  instead of as network errors — closing every path that orphaned or
  duplicated backend install rows.
- Serialize sends behind a gate; run opt-in/opt-out transitions on a FIFO
  queue that re-checks live state; ForceSend re-checks enabled under the
  gate.
- Require 3 x 403 before re-registering, with the count persisted in config
  (an in-memory count on a frequently restarted server would never trip and
  the install would 403 forever); reset it on success.
- Subtract the exact sent snapshot on period reset (no lost increments),
  clamped at zero so a boundary reset racing an in-flight send can never
  persist or ship a negative count.
- Validate the registration response shape; stop swallowing HTTP timeouts
  as silent "shutdown"; use InvariantCulture for all period dates (a Thai
  locale otherwise reports Buddhist-calendar years the dashboard would
  filter out as forged).

Config page
- First-opt-in cascade runs once and survives sessions: the configured flag
  now persists whenever the master was turned on this session, even if it
  is off again at save time — a deliberate category opt-out can no longer
  be silently re-cascaded over after a reload.
- Preview requests carry a monotonic token (overlapping auto-triggered
  responses can't race last-write-wins into a stale payload); readable
  preview errors instead of "[object Response]".

Public dashboard (docs)
- Treat every report-derived field as hostile: strict period-shape
  validation plus a trailing 45-day aggregation window (a single forged
  "today" period could previously hide every genuine install's rows),
  numeric coercion + 0-100 clamp on all count/percentage fields, prototype-
  safe key lookups, String() guards, per-section error isolation, row
  limits with explicit order= (deterministic truncation), and display caps
  ranked by count — an alphabetical cut would have let forged names evict
  the genuine entries.
- Fix isTargetMismatch for Jellyfin 13+; correct "three views" -> four and
  the garbled "Always included" note; document the derived/normalized
  fields.

Tooling
- generate_config_flag_groups.py: fix the label-dedup key comparison and
  remove dead handle_entityref (output byte-identical).

Developed with AI assistance (Claude Code); all changes reviewed and
live-tested on Jellyfin 10.11.11.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013sScjM7juAei2VwvrakmbS
@n00bcodr
n00bcodr merged commit ad003de into main Aug 30, 2026
6 checks passed
@n00bcodr
n00bcodr deleted the analytics branch August 30, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants