Adds the cookieless Privatus Analytics
tracking script to your Java web app. The Spring Boot starter gives you a
Thymeleaf dialect, a helper for JSP and FreeMarker, and an optional filter that
puts the script in the <head> for you.
Privatus Analytics counts visits, sources, pages and conversions without cookies, local storage or fingerprinting, so analytics does not need a consent banner by itself. Check with your own counsel for your country.
The library only prints the browser script and some HTML attributes. It never sends events from your server, sets no cookies, and never reads IP addresses or browser details. You need a Privatus Analytics account to see reports, and there is a free plan. Sign up at https://privatusanalytics.com.
There are two packages:
privatus-analytics-spring-boot-starter: for Spring Boot 3 apps. Most people want this one.privatus-analytics-core: the plain Java part, with no dependencies. Use it with any other framework.
- Java 17 or later.
- Spring Boot 3.2 to 3.5, with Spring MVC (for the starter).
- A Privatus Analytics site id (it starts with
pa_). You find it under Site settings, Tracking.
Maven:
<dependency>
<groupId>com.privatusanalytics</groupId>
<artifactId>privatus-analytics-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>Gradle:
implementation "com.privatusanalytics:privatus-analytics-spring-boot-starter:1.0.0"For the full setup guide and every tracker option, see the Privatus Analytics docs.
Set your site id with an environment variable:
PRIVATUS_SITE_ID=pa_YOURSITEIDor in application.properties:
privatus.analytics.site-id=pa_YOURSITEIDPut the tag in the <head> of your layout:
<html xmlns:th="http://www.thymeleaf.org"
xmlns:privatus="https://privatusanalytics.com/thymeleaf">
<head>
<title>My app</title>
<th:block privatus:analytics/>
</head>With the prod or production profile active, it prints:
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID"></script>The xmlns:privatus line is optional. It only stops your editor from
complaining, and it is removed from the page.
Every request and model gets a privatusAnalytics helper:
<head>
<title>My app</title>
${privatusAnalytics.tag}
</head><head>
<title>My app</title>
${privatusAnalytics.tag?no_esc}
</head>FreeMarker escapes HTML for you, so ?no_esc tells it the tag is already safe.
With only privatus-analytics-core, build the settings once and print the tag:
PrivatusSettings settings = PrivatusSettings.builder()
.siteId("pa_YOURSITEID") // or the PRIVATUS_SITE_ID environment variable
.environment(System.getenv("APP_ENV")) // the tag prints only for "production"
.build();
String html = Tag.render(settings);The tag prints nothing when there is no site id, and nothing outside production (see below).
If your site uses a Content Security Policy nonce (a random code that tells
the browser which inline scripts to trust), put it in the request attribute
cspNonce and the starter adds it to every script element. Use
privatus.analytics.nonce-attribute to read another attribute name. With
privatus-analytics-core, call .nonce("...").
If you would rather not touch your layouts, switch on the filter:
privatus.analytics.auto-inject=trueIt adds the tag just before </head> in uncompressed text/html responses
with status 200. It skips Ajax (background) requests, pages without a
<head>, and pages that already have the tag. Responses that carry a
Content-Encoding header are left alone. Compression set with
server.compression.enabled happens after the filter, so it works with it.
Other responses stream through untouched.
Every setting is optional except the site id.
# application-prod.properties
privatus.analytics.site-id=pa_YOURSITEID
privatus.analytics.script-host=https://privatusstats.com
privatus.analytics.modules=engage,auto,vitals,clicks,ask
privatus.analytics.excluded-paths=/admin/*,/preview/*
privatus.analytics.domains=example.com,www.example.com
privatus.analytics.spa=auto
privatus.analytics.manual=false
privatus.analytics.auto-inject=false
privatus.analytics.enabled-profiles=prod,production
privatus.analytics.extra-attributes.data-mask=/orders/*->/orders/:id
privatus.analytics.props.app_version=4.2
privatus.analytics.queue-stub=false| Setting | Default | What it does |
|---|---|---|
site-id |
PRIVATUS_SITE_ID |
The data-site attribute. Nothing prints without it |
script-host |
https://privatusstats.com |
Where /js/pa.js is served (or PRIVATUS_SCRIPT_HOST). Change it only if you host your own copy |
modules |
tracker default (engage) |
The data-modules attribute. Setting it replaces the default, so keep engage for time on page and scroll depth. An empty value (modules=) loads none |
excluded-paths |
none | The data-exclude attribute. Path patterns the tracker never counts |
domains |
all hosts | The data-domains attribute. Count only these hostnames |
spa |
auto |
The data-spa attribute, for single-page apps: auto, history, hash or off |
manual |
false |
The data-manual attribute. true turns off the automatic pageview |
enabled-profiles |
prod,production |
Spring profiles that get the tag |
enabled |
not set | true or false forces the tag on or off in every profile |
auto-inject |
false |
Adds the tag with a servlet filter |
extra-attributes |
none | Any other data-* attribute |
props |
none | Page properties for every page (see below) |
queue-stub |
false |
Prints the queue stub before the tag (see below). It still prints when the tag is off, so privatus.track calls never fail |
nonce-attribute |
cspNonce |
The request attribute that holds your nonce |
expose-model-attribute |
true |
Adds the privatusAnalytics helper to requests and models |
Each tracker attribute is explained in the script attributes reference, and the modules in Modules.
You can also change a setting for one page:
<th:block privatus:analytics="${ {modules: {'engage','auto'}, excludedPaths: {'/preview/*'}} }"/>${privatusAnalytics.tagWith({'modules': ['engage', 'auto']})}${privatusAnalytics.tagWith({"modules": ["engage", "auto"]})?no_esc}Tag.render(settings.toBuilder().modules("engage", "auto").build());Only the prod and production profiles get the tag by default, so your own
clicking around in development and tests never reaches your reports. To see
the tag while you build, set privatus.analytics.enabled=true. The tracker
also ignores localhost unless you pass data-allow-local or data-debug
(see Debug mode):
privatus.analytics.extra-attributes.data-debug=trueUse excluded-paths for admin areas. To leave out signed-in staff, print the
tag only for other people:
<th:block th:unless="${currentUser?.admin}" privatus:analytics/>Privatus can show which buttons people press, which forms they send, and how that differs by plan. These helpers print plain HTML, so they print in every profile. That keeps your development and production pages the same. The tracker only sends anything where the tag is on.
<button privatus:event="Export CSV" privatus:props="${ {format:'csv', source:'toolbar'} }">
Export
</button>prints
<button data-privatus-event="Export CSV" data-privatus-prop-format="csv" data-privatus-prop-source="toolbar">
Export
</button>On a <form>, the event is sent when the form is submitted:
<form method="post" action="/signup" privatus:event="Signup" privatus:props="${ {plan:'pro'} }">In JSP and FreeMarker:
<button ${privatusAnalytics.eventAttrs('Export CSV', {'format': 'csv'})}>Export</button><button ${privatusAnalytics.eventAttrs("Export CSV", {"format": "csv"})?no_esc}>Export</button>In plain Java: PrivatusAnalytics.eventAttrs("Export CSV", Map.of("format", "csv")).
The rules: the event name is trimmed and cut to 120 characters. An empty name
prints nothing. Property names are lowercased, and any character other than
a-z, 0-9 and _ becomes _ (cut to 64 characters). Up to 30 properties
are kept. Values become text: true and false, numbers as written, lists
joined with commas. Empty (null) values are left out, and values are cut to
500 characters. Everything is escaped, so it is safe to print.
<a href="/signup" privatus:click="pricing.cta">Start free</a>prints data-privatus-click="pricing.cta". Use 1 to 100 letters, digits,
_, ., : or -. Any other name prints nothing (and a warning in the log
while the tag is off). In JSP: ${privatusAnalytics.clickAttr('pricing.cta')}.
In plain Java: PrivatusAnalytics.clickAttr("pricing.cta").
Page properties are sent with every pageview and event on the page, so you can split any report by them. Good ones are plan, role, app version, feature flags and account size.
Never put names, emails, user ids or anything else that points to one person in page properties. Privatus counts visitors without any id. Put groups (plan, role), not people. Emails and long ids are also removed by the server.
Set the same properties for every page in your settings:
privatus.analytics.props.app_version=4.2Set them per request with a bean:
@Bean
PrivatusPagePropsProvider pageProps(AccountService accounts) {
return request -> {
Account account = accounts.current(request);
return account == null ? null : Map.of("plan", account.plan(), "role", account.role());
};
}Or for one page: <th:block privatus:analytics="${ {props: {section:'docs'}} }"/>.
These are added on top of each other, so the later ones win.
The tag then prints a small script before the tracker:
<script>window.privatusPageProps=function(){return {"plan":"pro","role":"admin"}}</script>
<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID" data-props="privatusPageProps"></script>Names follow the same rules as event properties. Numbers and true/false
stay as they are, and lists stay lists (up to 30 items).
privatus.track("Feature used", { feature: "export" });If your code can run before the tracker loads, switch on the queue stub. It keeps early calls and sends them once the tracker is ready:
privatus.analytics.queue-stub=trueIt prints this before the tag (with your nonce if you have one):
<script>window.privatus=window.privatus||{q:[]};['track','pageview','props','optOut','optIn','ready'].forEach(function(m){window.privatus[m]=window.privatus[m]||function(){window.privatus.q.push([m,Array.prototype.slice.call(arguments)])}})</script>The stub prints even where the tag is off (outside the prod profile, with
enabled=false, or with no site id). Then only the stub prints: no tracker
and no page properties. So you can always call privatus.track(...). In
development the calls just wait in the queue and are never sent. The filter
adds nothing when the tag is off. See the JavaScript API
docs.
Use "Thing done" names, like Report exported, Invite sent or
Plan upgraded. Keep the name short and the same everywhere, and put the
details in properties: Report exported with format: csv, not
Report exported as CSV. This keeps your event list short and easy to read.
Then build on your events:
- Funnels show where people drop out between steps.
- Click maps show what people click on a page.
- Goals turn an event into a conversion you can follow.
- Events explains every event option.
The script sets no cookies and stores nothing in the browser. This library sends nothing from your server. See the privacy documentation for what is collected.
If you have a privacy policy, it should say which tools handle your visitors' data. You can paste this:
This website uses Privatus Analytics to count visits. It does not use cookies and does not follow you from one website to another. Your IP address is used for a moment to count visits and is never stored.
Change it to fit your site. The privacy policy generator makes a longer version with your site's name in it.
The tests run in Docker so your local Java does not matter:
./run-docker.shIt runs the JUnit tests (with a small Spring Boot test app for the starter), checks the code format with Spotless (google-java-format), and builds the jars, sources jars and javadoc jars. To fix the format:
docker run --rm -v "$PWD":/app -v privatus-java-m2:/root/.m2 -w /app maven:3.9-eclipse-temurin-21 mvn -B spotless:applyBug reports and pull requests are welcome at https://github.com/Privatus-Analytics/java.
MIT. See LICENSE. Privatus Analytics is a product of Two Phase LLC, Wyoming, USA.