Privatus Analytics for R

Adds the cookieless Privatus Analytics tracking script to Shiny apps, R Markdown and Quarto documents. It is built on htmltools, so it works anywhere htmltools tags work.

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 package only prints the browser script and HTML attributes. It never sends events from R, never makes web requests 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

Install

From the Privatus R-universe (a package server for R):

install.packages("privatusanalytics", repos = c("https://privatus-analytics.r-universe.dev", getOption("repos")))

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

Set your site id with an environment variable, for example in .Renviron:

PRIVATUS_SITE_ID=pa_YOURSITEID

or with an R option, for example at the top of app.R:

options(privatus.site_id = "pa_YOURSITEID")

Add the tag

Shiny

Put privatus_analytics() in the head of your page:

library(shiny)
library(privatusanalytics)

ui <- fluidPage(
  tags$head(privatus_analytics()),
  titlePanel("My app")
)

In production it prints:

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

R Markdown and Quarto

R Markdown and Quarto can add an HTML file to the head of every page. use_privatus_header() writes the tag to such a file:

privatusanalytics::use_privatus_header("_privatus.html")

Then point your document at it. In R Markdown:

output:
  html_document:
    includes:
      in_header: _privatus.html

In Quarto (_quarto.yml or the document header):

format:
  html:
    include-in-header: _privatus.html

Write the file each time you build, so it follows your settings. For Quarto, a pre-render script does that. Make a file privatus.R with the line above, and add it to _quarto.yml:

project:
  pre-render: privatus.R

For R Markdown, run it in your build script before rmarkdown::render(). When the tag is off, the file is written empty (or with only the queue stub, if you turned it on), so the build still works.

You can also print the tag straight from a code chunk. It then goes in the body, not the head, which works too:

```{r, echo = FALSE}
privatusanalytics::privatus_analytics()
```

The tag prints nothing when there is no site id, and nothing outside production (see below). With queue_stub = TRUE it then prints only the queue stub (see Track from your own JavaScript). If your site uses a Content Security Policy nonce, pass it with nonce.

Adding it for you

R has no shared web server layer, so the package cannot insert the tag into every page by itself. In Shiny, add it to your ui once. For documents, use_privatus_header() and the header file do the same job for every page you build.

Configuration

Every setting is optional except the site id. Set them once with R options, for example at the top of app.R or in .Rprofile:

options(
  privatus.site_id        = "pa_YOURSITEID",             # or PRIVATUS_SITE_ID
  privatus.script_host    = "https://privatusstats.com", # or PRIVATUS_SCRIPT_HOST
  privatus.modules        = c("engage", "auto", "vitals", "clicks", "ask"),
  privatus.excluded_paths = c("/admin/*", "/preview/*"),
  privatus.domains        = c("example.com", "www.example.com"),
  privatus.spa            = "auto",   # auto, history, hash or off
  privatus.manual         = FALSE,    # TRUE turns off the automatic pageview
  privatus.enabled        = NULL,     # TRUE or FALSE forces the tag on or off
  privatus.extra_attributes = list("data-mask" = "/orders/*->/orders/:id"),
  privatus.props          = function(request) list(plan = "pro"),
  privatus.queue_stub     = FALSE,
  privatus.nonce          = NULL
)
Option Default What it does
privatus.site_id PRIVATUS_SITE_ID The data-site attribute. Nothing prints without it
privatus.script_host https://privatusstats.com Where /js/pa.js is served. Change it only if you host your own copy
privatus.modules tracker default (engage) The data-modules attribute. Setting it replaces the default, so keep engage for time on page and scroll depth. character(0) loads none
privatus.excluded_paths none The data-exclude attribute. Path patterns the tracker never counts
privatus.domains all hosts The data-domains attribute. Count only these hostnames
privatus.spa auto The data-spa attribute
privatus.manual FALSE The data-manual attribute
privatus.enabled NULL TRUE or FALSE forces the tag on or off. NULL means on only in production (see below)
privatus.extra_attributes none Any other data-* attribute, as a named list
privatus.props none Page props sent with every pageview and event. A named list, or a function that gets the request. See below
privatus.queue_stub FALSE Prints a small script first, so privatus.track calls made before the tracker loads are kept and sent later
privatus.nonce none A Content Security Policy nonce for the script tags

Each tracker attribute is explained in the script attributes reference, and the modules in Modules.

Every option is also an argument, so you can change a setting for one page:

privatus_analytics(modules = c("engage", "auto"), excluded_paths = "/preview/*")

Development and test

R has no single setting for "this is the live site". So the tag is on only when the PRIVATUS_ENV or R_CONFIG_ACTIVE environment variable is production. Your own clicking around while you build is never counted.

If you deploy to shinyapps.io or Posit Connect, set PRIVATUS_ENV to production in the app's environment variables (on Posit Connect: the Vars tab; on shinyapps.io: rsconnect::deployApp(envVars = ...) or the app settings). If you use the config package, R_CONFIG_ACTIVE=production works too. Or set options(privatus.enabled = TRUE) in the deployed app. For documents, set PRIVATUS_ENV=production where you build the live site.

To see the tag while you build, set options(privatus.enabled = TRUE). The tracker also ignores localhost unless you pass data-allow-local or data-debug (see Debug mode):

options(privatus.extra_attributes = list("data-debug" = "true"))

Leaving people out

Use data-exclude through excluded_paths for admin areas. To leave out your own staff, add the tag only when the visitor is not one of them. In Shiny, make ui a function so it runs for each visitor:

ui <- function(req) {
  fluidPage(
    tags$head(if (!is_staff(req)) privatus_analytics()),
    titlePanel("My app")
  )
}

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 R code 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.

Events on buttons, links and forms

privatus_event() adds the attributes to any htmltools tag, including Shiny inputs:

privatus_event(tags$button("Export"), "Report exported", format = "csv", source = "toolbar")
privatus_event(actionButton("go", "Run report"), "Report run")

The first one prints:

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

privatus_event_data() returns the same attributes as a named list, so you can splice them into a tag with !!!. privatus_event_attrs() returns them as HTML text, for a tag you write by hand in R Markdown:

tags$a(href = "/guide", "Read the guide", !!!privatus_event_data("Guide opened"))
<button `r privatusanalytics::privatus_event_attrs("Report exported", format = "csv")`>Export</button>

On a form, the event is sent when the form is submitted:

privatus_event(tags$form(action = "/join", ...), "Newsletter joined", list = "weekly")

The rules:

Click maps

A click map shows where people click on a page. Give the important parts a name so they stay easy to find, even when the page changes:

privatus_click(tags$a(href = "/pricing", "See plans"), "pricing.cta")

privatus_click_data() and privatus_click_attr() give the same as a list and as HTML text. A name may use letters, digits and _ . : -, up to 100 characters. Anything else adds nothing, and outside production you get an R warning so you can fix the name.

Page props: plan, role and other groups

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.

Set them for every page with an option. It can be a function that gets the request (in Shiny, pass req from a ui function):

options(privatus.props = function(request) {
  list(plan = account_plan(request), role = user_role(request), app_version = "2.4")
})

ui <- function(req) {
  fluidPage(tags$head(privatus_analytics(request = req)), ...)
}

A plain named list works too. You can also set or change props for one page. They are merged over the configured ones, and NULL or NA removes one:

privatus_analytics(props = list(section = "billing", role = NULL))

The tag then prints a small script before the tracker, with your Content Security Policy nonce when you pass one, and points the tracker at it:

<script nonce="...">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. Vectors longer than one, and lists, stay lists (up to 30 items). To send one value as a list, wrap it in list(). 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

For things that are not a click or a submit, call the tracker from your JavaScript, as the JavaScript API docs explain:

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

If your code may run before the tracker has loaded, switch on the queue stub. It prints a tiny script before the tracker that keeps your calls in a list and sends them once the tracker is ready:

options(privatus.queue_stub = TRUE)

or for one page: privatus_analytics(queue_stub = TRUE). The stub prints even when the tag is off (in development, or with no site id). Then the tag prints only the stub, with no tracker, so your privatus.track(...) calls never fail. They wait in the list and are never sent.

Track from a Shiny server function

Many things in a Shiny app happen in the server function, like a download or a model run. Add privatus_shiny() to your page. It is a tiny script that listens for messages from your server. Then call privatus_track():

ui <- fluidPage(
  tags$head(privatus_analytics(), privatus_shiny()),
  downloadButton("export", "Export")
)

server <- function(input, output, session) {
  output$export <- downloadHandler("data.csv", function(file) {
    privatus_track("Report exported", format = "csv")
    write.csv(mtcars, file)
  })
}

Your R server still does not talk to Privatus. The message goes to the visitor's own browser over the Shiny connection, which calls privatus.track("Report exported", {format: "csv"}), just like a click would. Props follow the page props rules. When the tracker is not on the page (for example in development) nothing happens.

See the results

Privacy

The script sets no cookies and stores nothing in the browser. The event, click and props helpers only print HTML attributes and a small script. Keep people out of your props and event props: no names, emails or user ids. 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.

Development

The checks run in Docker so your local R does not matter:

./run-docker.sh

It builds the help pages with roxygen2, runs lintr and the testthat tests, then R CMD build and R CMD check --as-cran. The first run installs the packages it needs into .rlib/, so later runs are fast.

Contributing

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

License

MIT. See LICENSE.md. Privatus Analytics is a product of Two Phase LLC, Wyoming, USA.