Frostyh

Retention Lens

See where and why new players leave: a day 1 to day 7 funnel, a quit heatmap and what keeps players.

Price
Coming soon
Runs on
Paper, Java 21
Version
v1.0.0

Read the docs

Retention Lens coverComing soon

In game

See it on a real server

5 screenshots, all taken on a real server. Click a screenshot to see it full size.

Retention Lens: overview
Retention Lens: insights
Retention Lens: quits
Retention Lens: heatmap map
Retention Lens: journey

01 / 05

Overview

Most new players leave in their first hour. Retention Lens shows you where they were, how long they lasted and what the players who stayed did differently.

Every server owner knows the feeling: twenty new players a day, and a week later almost none of them are still around. Retention Lens follows every player who joins for the first time and answers three questions in one staff screen: how far do new players get, where do they quit, and which first steps go together with players who come back.

In short

  • A funnel from first join to "back after a week", so you see the exact stage you lose players at
  • The spots new players stood on when they left, busiest first, with a teleport button and a heatmap map item
  • "Players who set a home in their first day came back after a week 3x as often": comparisons computed from your own players, with the sample size shown
  • One player's whole journey: first join, sessions, milestones and where they quit
  • A CSV export of everything for your own spreadsheets
Name in /plugins
RetentionLens
Latest version
1.0.0
Runs on
Paper or Purpur, Java 21
BuiltByBit
Coming soon
What changed in 1.0.0
  • Follows every new player from their first join: sessions, playtime, milestones and where they quit.
  • Overview: the funnel from first join to back after a week, players who left within 10 minutes, the typical first session.
  • What keeps players: milestones in the first day compared with who came back after a week, with sample sizes.
  • Where they quit: hotspots per world with teleport buttons, and a heatmap map item.
  • One player's journey, CSV export, and /retentionlens milestone for other plugins.

Features

  1. 01What makes it different

    • Honest numbers: every stage only counts players old enough to have reached it. A busy last day never drags your week down, and a player who joined an hour ago is not counted as "never came back"
    • Comparisons only use what a player did in their first 24 hours (you can change the window), so long-time players do not make every milestone look magical. Below a minimum sample the screen says "needs more data" instead of guessing
    • Players who joined before you installed it are left out of the charts, because their first days were never seen
    • Privacy: no IP address is ever read or stored, everything stays in one SQLite file on your server, and names can be switched off
  2. 02What you get

    • Overview in a native dialog: new players in the window (7, 14, 30, 90 or 365 days), the funnel (joined, stayed 10 minutes, played an hour, came back on day 2 or later, back after a week), how many left within 10 minutes and never came back, the typical first session and the strongest signal
    • Funnel and playtime: every stage with its sample, and how long new players play in total, in six buckets
    • What keeps players: each milestone done in the first day against not done, with how many in each group came back after a week
    • Where they quit: quit positions grouped into squares (32 blocks by default), per world or all worlds, how many of them happened in a first session, and a teleport button for each
    • Heatmap map item: a map of a world, brighter where more new players quit
    • Milestones you choose: first mob kill, first player kill, first death, first chat message, any or one specific advancement, a command (claim, sethome, shop), a playtime goal, or any milestone another plugin records with /retentionlens milestone <player> <id> (a store purchase, a quest, a tutorial step)
    • Works with First-Join Tour: every tour step is recorded as a milestone automatically, so you can see whether finishing the tour keeps players
    • CSV export: players with every milestone date, and every quit position
    • Every word is yours: all messages and buttons in config.yml, in MiniMessage, with one colour theme

Installation

Requirements

Server
Paper, or a Paper fork such as Purpur. Spigot and CraftBukkit won't work, because it uses Paper-only APIs.
Minecraft
plugin.yml api-version 1.21; built against Paper API 1.21.11
Java
21 or newer
Needs
No other plugin
Works with
Nothing optional
Libraries
org.xerial:sqlite-jdbc:3.53.2.1, downloaded by the server on first start

Steps

  1. Download RetentionLens.jar from BuiltByBit. There's no licence key to enter.
  2. Stop the server and put the jar in the plugins folder.
  3. Start the server. The plugin writes plugins/RetentionLens/config.yml with the defaults listed under Configuration.
  4. Change config.yml to fit your server.
  5. Run /retentionlens reload (read config.yml again) to apply your changes, or restart the server.

Commands

Straight from plugin.yml. Where the permission column says "not set", plugin.yml puts no permission on the command itself; the nodes the plugin checks are under Permissions.

CommandAliasesUsageWhat it doesPermission
/retentionlens/rlens /retention/retentionlens [open|funnel|insights|quits|heatmap|player|export|milestone|reload|help]Open the Retention Lens overview, or run one of its tools.not set

Every subcommand

/retentionlens                        The overview (aliases: /rlens, /retention)
/retentionlens funnel                 The funnel and the playtime buckets
/retentionlens insights               What keeps players
/retentionlens quits                  Where new players quit
/retentionlens heatmap [world]        A heatmap map item
/retentionlens player <name>          One player's journey
/retentionlens export [days]          CSV export (works from the console)
/retentionlens milestone <player> <id>  Record a milestone (console, other plugins, store webhooks)
/retentionlens reload                 Read config.yml again

Permissions

From plugin.yml.

NodeDefaultWhat it allowsIncludes
retentionlens.viewoperatorsOpen the overview, the funnel, the insights, the quit list and the player screen.none
retentionlens.heatmapoperatorsGet the quit heatmap as a map item.none
retentionlens.teleportoperatorsTeleport to a quit hotspot from the quit screen.none
retentionlens.exportoperatorsExport the data as CSV files.none
retentionlens.adminoperatorsRecord milestones by command and reload the config.retentionlens.view retentionlens.heatmap retentionlens.teleport retentionlens.export

Placeholders

This plugin doesn't add any PlaceholderAPI placeholders.

Configuration

The default files exactly as they ship in the jar, comments included. The plugin writes them to plugins/RetentionLens/ the first time it starts.

config.yml

KeyDefaultThe comment above it
quitssection, 4 keysWhat counts as a quit worth mapping.
milestonessection, 11 keysThe first time a player does one of these, it is stored with the time. The insights screen compares players who did a milestone in their first day with those who did not. Types:
chartssection, 1 keynone
insightssection, 2 keysnone
heatmapsection, 1 keynone
datasection, 2 keysnone
formatsection, 1 keynone
debugfalseLogs every milestone to the console. For setting up command matches.
themesection, 12 keysnone
messagessection, 89 keysnone
plugins/RetentionLens/config.yml299 lines
# Retention Lens by Frostyh: where and why new players leave your server.
#
#   /retentionlens (or /rlens) opens the overview: the funnel from first join to day 7, how long
#   new players play, where they stood when they quit, and which first steps go with players who
#   stay. Staff only (retentionlens.view).
#
# Privacy: no IP address is ever read or stored. Names are kept for the staff screens (switch that
# off under data.store-names). Everything lives in plugins/RetentionLens/retention.db on your own
# server; nothing is sent anywhere.
#
# Players who first joined BEFORE you installed Retention Lens are left out of every chart, because
# their first days were never seen. The charts fill up as new players arrive.
#
# Text is MiniMessage, written against the theme colours (<accent>, <text>, <muted>, ...).
# Updating: keys added in a new version work straight away, even with an older config.yml.

# ============================================================================
#  ███ ███   ██   ███ █  █ ███ █   █  ███
#   █  █  █ █  █ █    █ █   █  ██  █ █
#   █  ███  ████ █    ██    █  █ █ █ █ ██
#   █  █ █  █  █ █    █ █   █  █  ██ █  █
#   █  █  █ █  █  ███ █  █ ███ █   █  ███
# ============================================================================
# What counts as a quit worth mapping.
quits:
  # Quit positions are recorded for a player's first this-many days. 0 records every quit.
  track-days: 14
  # Worlds whose quits are never recorded (lobbies you send everyone to, for example).
  ignored-worlds: []
  # The quit screen groups quits into squares of this many blocks.
  cell-size: 32
  # How many squares the quit screen lists.
  top: 8

# ============================================================================
#  █   █ ███ █    ████  ███ ███  ██  █   █ ████  ███
#  ██ ██  █  █    █    █     █  █  █ ██  █ █    █
#  █ █ █  █  █    ███   ██   █  █  █ █ █ █ ███   ██
#  █   █  █  █    █       █  █  █  █ █  ██ █       █
#  █   █ ███ ████ ████ ███   █   ██  █   █ ████ ███
# ============================================================================
# The first time a player does one of these, it is stored with the time. The insights screen
# compares players who did a milestone in their first day with those who did not.
#
# Types:
#   kill-player   killed another player           kill-mob   killed a mob
#   death         died                            chat       sent a chat message
#   advancement   got an advancement: "advancement" names one key, empty means any (recipes never count)
#   command       ran a command starting with one of "match" ("/claim" matches "/claim 10", not "/claimlist")
#   playtime      reached "minutes" of total playtime
# Any other plugin can record its own milestone with:
#   /retentionlens milestone <player|uuid> <id>
# for example from a store or a quest plugin. Give it a label here to show a nicer name.
milestones:
  first-mob-kill:
    type: kill-mob
    label: "Killed a mob"
  first-player-kill:
    type: kill-player
    label: "Killed a player"
  first-death:
    type: death
    label: "Died"
  first-chat:
    type: chat
    label: "Said something in chat"
  first-advancement:
    type: advancement
    advancement: ""
    label: "Got an advancement"
  smelted-iron:
    type: advancement
    advancement: "minecraft:story/smelt_iron"
    label: "Smelted iron"
  claimed-land:
    type: command
    match: ["/claim", "/rg claim", "/f claim"]
    label: "Claimed land"
  set-home:
    type: command
    match: ["/sethome"]
    label: "Set a home"
  opened-shop:
    type: command
    match: ["/shop", "/ah", "/auction"]
    label: "Opened the shop"
  played-30m:
    type: playtime
    minutes: 30
    label: "Played 30 minutes"
  # A milestone recorded by another plugin through the command above:
  first-purchase:
    type: external
    label: "Bought something in the store"

# ============================================================================
#   ███ █  █  ██  ███  ███  ███
#  █    █  █ █  █ █  █  █  █
#  █    ████ ████ ███   █   ██
#  █    █  █ █  █ █ █   █     █
#   ███ █  █ █  █ █  █  █  ███
# ============================================================================
charts:
  # The window the screens look at by default, in days. Staff can switch between 7, 14, 30, 90
  # and 365 on the overview.
  window-days: 30

# ============================================================================
#  ███ █   █  ███ ███  ███ █  █ ███  ███
#   █  ██  █ █     █  █    █  █  █  █
#   █  █ █ █  ██   █  █ ██ ████  █   ██
#   █  █  ██    █  █  █  █ █  █  █     █
#  ███ █   █ ███  ███  ███ █  █  █  ███
# ============================================================================
insights:
  # A milestone counts for "did it" when it happened within this many hours of the first join.
  # Counting it at any time would flatter every milestone: players who stay longer simply have
  # more time to do everything.
  within-hours: 24
  # Both groups need at least this many players before a comparison is shown. Below it, the
  # milestone is listed as "needs more data" with both counts.
  min-sample: 15

# ============================================================================
#  █  █ ████  ██  ███ █   █  ██  ███
#  █  █ █    █  █  █  ██ ██ █  █ █  █
#  ████ ███  ████  █  █ █ █ ████ ███
#  █  █ █    █  █  █  █   █ █  █ █
#  █  █ ████ █  █  █  █   █ █  █ █
# ============================================================================
heatmap:
  # Blocks of empty space around the outermost quit on the map item.
  padding: 32

# ============================================================================
#  ███   ██  ███  ██
#  █  █ █  █  █  █  █
#  █  █ ████  █  ████
#  █  █ █  █  █  █  █
#  ███  █  █  █  █  █
# ============================================================================
data:
  # Keep names for the staff screens and the CSV export. false stores a short id instead.
  store-names: true
  # Sessions and quit positions older than this many days are deleted at startup. 0 keeps all.
  keep-days: 365

format:
  # How dates are written on the screens (Java DateTimeFormatter pattern).
  date: "yyyy-MM-dd"

# Logs every milestone to the console. For setting up command matches.
debug: false

# ============================================================================
#  ███ █  █ ████ █   █ ████
#   █  █  █ █    ██ ██ █
#   █  ████ ███  █ █ █ ███
#   █  █  █ █    █   █ █
#   █  █  █ ████ █   █ ████
# ============================================================================
theme:
  accent: "#4fd1c5"     # headings and anything staff are meant to click
  accent2: "#9be8df"    # a lighter accent, for a second level of emphasis
  title: "#f4f7ff"      # screen titles
  text: "#c2ccdd"       # ordinary prose
  muted: "#6b7a94"      # hints, separators, anything the eye should skip
  value: "#ffffff"      # the numbers the screen exists to show
  good: "#57e08a"
  warn: "#ffc857"
  bad: "#ff6b6b"
  small-caps: false
  # Real Minecraft item pictures beside the buttons, from the client's own textures: no resource
  # pack. Needs a 1.21.9 or newer client; older ones see the words only.
  item-icons: true
  # A short sound on every click, open and result.
  sounds: true

# ============================================================================
#  █   █ ████  ███  ███  ██   ███ ████  ███
#  ██ ██ █    █    █    █  █ █    █    █
#  █ █ █ ███   ██   ██  ████ █ ██ ███   ██
#  █   █ █       █    █ █  █ █  █ █       █
#  █   █ ████ ███  ███  █  █  ███ ████ ███
# ============================================================================
messages:
  prefix: "<accent><bold>Retention</bold></accent> <muted>»</muted> "

  # ---- general ----
  no-permission: "<bad>You cannot do that.</bad>"
  players-only: "<bad>Only players can open the screens. The console can use export and milestone.</bad>"
  usage: "<text>Usage: </text><value>{usage}</value>"
  reloaded: "<good>Reloaded.</good> <muted>{count} milestones.</muted>"
  menu-error: "<bad>That screen could not load. The console has the details.</bad>"
  menu-back: "<white><icon:back></white>Back"
  menu-close: "<white><icon:close></white>Close"
  yes: "<good>yes</good>"
  "no": "<muted>no</muted>"

  # ---- overview ----
  title-overview: "<accent>Retention Lens</accent>"
  overview-head: "<title>New players, last {days} days</title> <muted>(tracking since {since})</muted>"
  overview-empty: "<text>No new players in this window yet. The charts fill up as players join for the first time.</text>"
  overview-cohort: "<text>Joined for the first time: </text><value>{count}</value>"
  stage-line: "<text>{stage}</text>  <accent>{bar}</accent> <value>{pct}</value> <muted>{reached}/{eligible}</muted>"
  stage-wait: "<text>{stage}</text>  <muted>not measurable yet: nobody in this window is old enough</muted>"
  stage-joined: "Joined          "
  stage-stayed-10m: "Stayed 10 min   "
  stage-played-1h: "Played 1 hour   "
  stage-day-2: "Came back day 2+"
  stage-day-7: "Back after a week"
  overview-quick: "<warn>Left within 10 minutes and never came back:</warn> <value>{count}</value> <muted>({pct} of those who joined a day ago or more)</muted>"
  overview-median: "<text>Typical first session: </text><value>{time}</value>"
  overview-top-insight: "<text>Strongest signal:</text> {line}"
  menu-funnel: "<white><icon:funnel></white>Funnel and playtime"
  menu-funnel-tip: "Every stage with its sample, and how long new players play in total"
  menu-insights: "<white><icon:insight></white>What keeps players"
  menu-insights-tip: "Milestones in the first day, compared with who came back after a week"
  menu-quits: "<white><icon:quit></white>Where they quit"
  menu-quits-tip: "The spots new players stood on when they left, busiest first"
  menu-player: "<white><icon:player></white>One player's journey"
  menu-player-tip: "First join, sessions, milestones and quit spots of one player"
  menu-heatmap: "<white><icon:heatmap></white>Heatmap map (this world)"
  menu-heatmap-tip: "A map item: brighter where more new players quit"
  menu-export: "<white><icon:export></white>Export CSV"
  menu-export-tip: "Writes players and quit positions to plugins/RetentionLens/exports/"
  menu-window: "<white><icon:time></white>Window: {days} days"
  menu-window-tip: "Switch between 7, 14, 30, 90 and 365 days"
  menu-help: "<white><icon:help></white>How to read this"

  # ---- funnel ----
  title-funnel: "<accent>Funnel and playtime</accent>"
  funnel-intro: "<muted>Each stage counts only players old enough to have reached it: day 2 needs a first join at least 2 days ago, a week needs 8 days.</muted>"
  buckets-head: "<title>Total playtime of new players</title>"
  bucket-line: "<text>{bucket}</text>  <accent>{bar}</accent> <value>{count}</value> <muted>{pct}</muted>"
  bucket-under-5m: "under 5 min "
  bucket-5-15m: "5 to 15 min "
  bucket-15-60m: "15 to 60 min"
  bucket-1-3h: "1 to 3 hours"
  bucket-3-10h: "3 to 10 h   "
  bucket-10h-plus: "10 h or more"

  # ---- insights ----
  title-insights: "<accent>What keeps players</accent>"
  insights-intro: "<muted>Players who did something in their first {hours} hours, against those who did not: how many came back after a week.</muted>"
  insight-line: "<value>{milestone}</value><text>: {did} came back</text> <muted>(of {didn})</muted><text> vs {not}</text> <muted>(of {notn})</muted> <accent>{times}</accent>"
  insight-only-them: "only they did"
  insights-none: "<text>No milestone has {min} players on both sides yet. The comparisons appear as the data grows.</text>"
  insights-waiting: "<muted>Needs more data (did/did not): {list}</muted>"
  insights-caveat: "<muted>These show what goes together, not what causes what. A strong signal is the place to test a change first.</muted>"

  # ---- quits ----
  title-quits: "<accent>Where they quit</accent>"
  quits-intro: "<text>{count} quits by new players in </text><value>{world}</value><muted>, in squares of {cell} blocks.</muted>"
  quits-all-worlds: "every world"
  quits-empty: "<text>No quits recorded in this window yet.</text>"
  quits-line: "<value>{n}.</value> <text>{world} around</text> <value>{x}, {z}</value><text>: {count} quits</text> <muted>({share}, {first} in a first session)</muted>"
  quits-go: "<white><icon:teleport></white>Go to {n}"
  quits-go-tip: "Teleport to the middle of that square"
  quits-world: "<white><icon:world></white>World: {world}"
  quits-world-tip: "Show one world at a time"
  world-missing: "<bad>There is no world called {world}.</bad>"

  # ---- heatmap ----
  heatmap-empty: "<text>No quits recorded in {world} in this window.</text>"
  heatmap-item-name: "<accent>Quit heatmap: {world}</accent>"
  heatmap-given: "<good>Here is the heatmap of {world}</good> <muted>({count} quits; brighter means more).</muted>"

  # ---- one player ----
  title-player: "<accent>One player's journey</accent>"
  player-intro: "<text>Type a name of a player who has joined since Retention Lens was installed.</text>"
  player-input: "Player name"
  player-find: "<white><icon:insight></white>Show"
  player-unknown: "<bad>No player called {name} is in the data.</bad>"
  title-journey: "<accent>{name}</accent>"
  journey-head: "<title>{name}</title> <muted>first joined {first}, last seen {seen}</muted> {online}"
  journey-online: "<good>online now</good>"
  journey-legacy: "<muted>Joined before tracking started, so this player is not in the charts.</muted>"
  journey-totals: "<text>Playtime </text><value>{playtime}</value><text> in </text><value>{sessions}</value><text> sessions. Came back on day 2+: {day2}, after a week: {day7}</text>"
  journey-no-milestones: "<muted>No milestones yet.</muted>"
  journey-milestone: "<white><icon:milestone></white><text>{milestone}</text> <muted>after {after}</muted>"
  journey-quit: "<white><icon:quit></white><text>Quit in {world} at</text> <value>{x} {y} {z}</value> <muted>after a {session} session</muted>"

  # ---- export and milestones ----
  export-started: "<text>Exporting...</text>"
  export-done: "<good>Exported {rows} players.</good> <muted>plugins/RetentionLens/exports/{players} and {quits}</muted>"
  export-failed: "<bad>The export could not be written. The console has the details.</bad>"
  milestone-bad-id: "<bad>A milestone id is 1 to 48 characters: lower case letters, digits and _ : . -</bad>"
  milestone-recorded: "<good>Recorded {id} for {player}</good> <muted>(only the first time counts).</muted>"

  # ---- help ----
  title-help: "<accent>How to read Retention Lens</accent>"
  help-lines:
    - "<title>The funnel</title> <text>follows everyone who joined for the first time in the window: did they stay 10 minutes, play an hour, come back on day 2 or later, come back after a week.</text>"
    - "<text>A stage only counts players old enough to have reached it, so a busy last day never drags the week down.</text>"
    - "<title>Left within 10 minutes</title> <text>is the number to watch first: these players never really saw your server.</text>"
    - "<title>Where they quit</title> <text>shows the spots new players stood on when they left. A spawn, a tutorial or a confusing first area usually tops the list.</text>"
    - "<title>What keeps players</title> <text>compares players who did a milestone in their first day with those who did not. It shows what goes together, not proof of what causes what.</text>"
    - "<muted>Commands: /rlens, /rlens funnel, /rlens insights, /rlens quits, /rlens heatmap [world], /rlens player <name>, /rlens export [days], /rlens milestone <player> <id>, /rlens reload</muted>"

FAQ

Which servers and versions does it run on?

It needs Paper or a Paper fork like Purpur, because it uses Paper-only APIs (the dialog API for its menus, Adventure text components). Spigot and CraftBukkit won't work. plugin.yml declares api-version 1.21, the jar is built against Paper API 1.21.11, and it needs Java 21 or newer.

Does it need other plugins?

No, it runs without any other plugin (plugin.yml has no hard dependencies). On first start the server downloads org.xerial:sqlite-jdbc:3.53.2.1, so it needs internet access then.

How do I apply config changes?

Edit plugins/RetentionLens/config.yml and run /retentionlens reload (read config.yml again). Restarting works too.

Does it run on Folia, or for Bedrock players?

I haven't tested it on Folia, and plugin.yml doesn't declare folia-supported. I haven't tested Bedrock players through Geyser either.

What licence do I get?

The BuiltByBit EULA shown on the listing. There's no licence key to enter, and the plugin never contacts a licence server.

Get it

Not on sale yet

Retention Lens is finished and waiting for its BuiltByBit listing. This page gets a buy button the day it goes up.

Ask about it on Discord

Also mine

Run a Discord too?

ZeroBot is my Discord bot: moderation, tickets, welcome messages, logs, backups and raid protection, all set up from a web dashboard. It's free to add; Premium plans unlock more.

Its Minecraft Link connects your server to your Discord through the free ZeroBot Link plugin (coming to Modrinth soon): a live status card, a chat bridge, join and death lines, and account linking. Outgoing HTTPS only, no port to open.

See ZeroBot

Custom work

Need something that isn't here? I make custom Paper plugins, change these ones to fit your server, and do server setup and config work. Small plugins start at 50 EUR; bigger jobs get a quote.

How commissions work