NAME

PrivatusAnalytics - add the cookieless Privatus Analytics script to Perl web apps

SYNOPSIS

use PrivatusAnalytics qw(event_attrs click_attr);

my $privatus = PrivatusAnalytics->new(
    site_id     => 'pa_YOURSITEID',
    environment => 'production',
);
print $privatus->tag;
# <script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID"></script>

print event_attrs('Report exported', { format => 'csv' });
# data-privatus-event="Report exported" data-privatus-prop-format="csv"

print click_attr('pricing.cta');
# data-privatus-click="pricing.cta"

DESCRIPTION

Adds the cookieless Privatus Analytics tracking script to your Perl web app. There is a plugin for Mojolicious (Mojolicious::Plugin::PrivatusAnalytics), a plugin for Dancer2 (Dancer2::Plugin::PrivatusAnalytics), a Plack middleware for any PSGI app (Plack::Middleware::PrivatusAnalytics), and this plain module you can use anywhere.

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.

This distribution only prints the browser script and a few HTML attributes. It never sends events from your server and sets no cookies. You need a Privatus Analytics account to see reports, and there is a free plan. Sign up at https://privatusanalytics.com.

REQUIREMENTS

Perl 5.20 or later. Optional: Mojolicious 9 or later, Dancer2 0.400000 or later, or Plack. A Privatus Analytics site id (it starts with pa_). You find it under Site settings, Tracking.

INSTALL

cpanm PrivatusAnalytics

For the full setup guide and every tracker option, see the Privatus Analytics docs.

Set your site id with the PRIVATUS_SITE_ID environment variable, or pass it as the site_id setting.

ADD THE TAG

Mojolicious

plugin PrivatusAnalytics => { site_id => 'pa_YOURSITEID' };

<head>
  %= privatus_analytics_tag
</head>

Dancer2

# config.yml
plugins:
  PrivatusAnalytics:
    site_id: pa_YOURSITEID

use Dancer2::Plugin::PrivatusAnalytics;

<head>
  [% privatus_analytics_tag() %]
</head>

Plack (any PSGI app)

builder {
    enable 'PrivatusAnalytics', site_id => 'pa_YOURSITEID';
    $app;
};

What it prints

In production it prints:

<script defer src="https://privatusstats.com/js/pa.js" data-site="pa_YOURSITEID"></script>

The tag prints nothing when there is no site id, and nothing outside production. If your app uses a Content Security Policy nonce (a random code that lets a script run), pass it as the nonce setting.

Or let it add the tag for you

Set auto_inject => 1 in the Mojolicious or Dancer2 plugin. The Plack middleware always does it. It adds the tag just before </head> in uncompressed text/html responses with status 200. It skips Ajax requests, pages without a <head>, and pages that already have the tag. Responses with a Content-Encoding header are left alone. The Content-Length header is fixed for you.

CONFIGURATION

Every setting is optional except the site id.

site_id

The data-site attribute. Falls back to $ENV{PRIVATUS_SITE_ID}. Nothing prints without it.

script_host

Where /js/pa.js is served. Falls back to $ENV{PRIVATUS_SCRIPT_HOST}, then https://privatusstats.com. A trailing slash is removed.

modules

The data-modules attribute, for example [qw(engage auto)]. undef keeps the tracker default (engage). [] loads none.

excluded_paths

The data-exclude attribute. Path patterns the tracker never counts.

domains

The data-domains attribute. Count only these hostnames.

spa

auto, history, hash or off. Only values other than auto print.

manual

True prints data-manual="true" and turns off the automatic pageview.

enabled

1 or 0 forces the tag on or off in every environment. undef uses enabled_environments.

enabled_environments

Environments that get the tag. Default ['production']. The Plack middleware uses ['deployment', 'production'].

environment

The current environment. The plugins set it from the framework. Otherwise it falls back to $ENV{PLACK_ENV}, then development. May be a code ref.

auto_inject

Adds the tag to the head of HTML pages. On by default in the Plack middleware.

extra_attributes

Any other data-* attribute, as a hash ref or an array ref of pairs.

props

Page props sent with every pageview and event. A hash ref, an array ref of pairs, or a code ref that gets the request. See "Page props".

queue_stub

True prints a small script first, so privatus.track calls made before the tracker loads are kept and sent later. It still prints when the tag is off (then nothing else prints), so those calls never fail.

nonce

The Content Security Policy nonce. A string, or a code ref that gets the request.

Lists can be array refs or comma separated text. Each tracker attribute is explained in the script attributes reference.

Development and test

Development and test requests are not counted by default. The tag is on when the Mojolicious mode is production, the Dancer2 environment is production, or PLACK_ENV is deployment or production for the Plack middleware. To see the tag while you build, set enabled => 1. The tracker also ignores localhost unless you pass data-allow-local or data-debug with extra_attributes.

Leaving people out

Use excluded_paths for admin areas. To leave out signed-in staff, print the helper only for other people.

METHODS

new(%settings)

Makes a settings object. Unknown settings are an error. Every setting has a get and set accessor of the same name.

tag(%options)

The tag HTML. When the tag is off it is only the queue stub (when queue_stub is on) or an empty string. Takes any setting as a per call override, plus request (passed to the props and nonce code refs) and props (merged over the configured props; undef removes one).

is_enabled($environment)

Whether the tag is on in that environment (default: the current one).

should_render

Whether a tag prints at all: there is a site id and the tag is on.

page_props($request, $extra)

The configured props for one request with $extra merged on top, as an array ref of pairs.

wants_inject(%response)

Used by the auto inject hooks. Takes status, content_type, content_encoding and requested_with.

FUNCTIONS

Exported on request (:all for every one).

event_attrs($name, \%props)

Event attributes as escaped HTML. Props can be a hash ref (keys sorted), an array ref of pairs or a list of pairs.

event_data($name, \%props)

The same attributes as a list of pairs.

click_attr($name)

The click map attribute as HTML, or an empty string when the name is not valid.

click_data($name)

The click map attribute as a list of pairs.

valid_click_name($name)

Whether a click map name is valid.

page_props_json(\%props)

Page props as JSON that is safe inside a script tag, or undef.

inject_tag($html, $tag)

Puts the tag before the first </head>, unless the page already loads /js/pa.js.

html_escape($text)

Escapes & < > " '.

PRODUCT ANALYTICS: SEE WHICH FEATURES PEOPLE USE

Privatus can show which buttons, forms and features people really use. You add a few attributes to your HTML. The tracker reads them in the browser and sends the events. Your server still sends nothing.

Name events well

Use a short "Noun verb" name, like Report exported or Invite sent. Put the details in props, not in the name. So write Report exported with format => 'csv', not Report exported as CSV. Then all exports add up in one place, and you can split them by format.

<button <%= privatus_event 'Report exported', { format => 'csv', source => 'toolbar' } %>>Export</button>

prints

<button data-privatus-event="Report exported" data-privatus-prop-format="csv" data-privatus-prop-source="toolbar">Export</button>

On a <form>, the event is sent when the form is submitted. The rules:

  • The name is trimmed and cut to 120 characters. An empty name prints nothing.

  • Prop keys are made lowercase. Anything that is not a letter, a digit or _ becomes _, so "Report Type" becomes report_type. Keys are cut to 64 characters.

  • At most 30 props. Values become text: numbers as written, lists joined with commas, \1 and \0 (or JSON::PP::true and JSON::PP::false) become true and false. undef values are left out. Values are cut to 500 characters.

  • Everything is escaped for HTML.

  • The attributes print in every environment, even when the tag is off.

Click maps

A click map shows where people click on a page. Give the important parts a name: <a href="/pricing" <%= privatus_click 'pricing.cta' %>>. A name may use letters, digits and _ . : -, up to 100 characters. Anything else prints nothing.

Page props

Page props are sent with every pageview and event on the page. Use them to split your reports by plan, role, app version, feature flag or account tier.

props => sub { my ($request) = @_; { plan => 'pro', role => 'admin' } }

The tag then prints a small script before the tracker and points the tracker at it:

<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>

Keys follow the same rules as event props. Numbers and true or false stay as they are, and lists stay lists (up to 30 items). The props are escaped so they are safe inside a script.

Never put names, emails, user ids or anything that points to one person in props. Privatus counts visitors without any id. Put groups (like the plan or the role), not people. The server also removes emails and long ids it finds.

Track from your own JavaScript

privatus.track("Feature used", { feature: "export" })

If your code may run before the tracker has loaded, set queue_stub => 1. It prints this tiny script before the tracker:

<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 (in development, with enabled => 0, or with no site id). Then only the stub prints, so you can always call privatus.track(...). In development the calls just wait in the queue and are never sent.

See the JavaScript API docs.

See the results

Funnels, Click maps, Goals and Events.

PRIVACY

The script sets no cookies and stores nothing in the browser. This distribution only prints HTML attributes and small scripts. Nothing here reads IP addresses or browser details, and nothing is sent from your server. See the privacy documentation.

SEE ALSO

Mojolicious::Plugin::PrivatusAnalytics, Dancer2::Plugin::PrivatusAnalytics, Plack::Middleware::PrivatusAnalytics, https://privatusanalytics.com/docs

CONTRIBUTING

Bug reports and pull requests are welcome at https://github.com/Privatus-Analytics/perl.

AUTHOR

Two Phase LLC, Wyoming, USA.

LICENSE

MIT. Copyright (c) 2026 Two Phase LLC. See the LICENSE file.