Try it Free

How We Made Watch Page Analytics Provider-Agnostic

We wanted analytics on our shared video watch pages. Who’s watching, when, from where. We added Umami support in the morning and ripped it out by the afternoon — not because it didn’t work, but because we’d hardcoded ourselves into a corner.

The first version: UMAMI_WEBSITE_ID

The initial implementation was a single environment variable:

UMAMI_WEBSITE_ID=9abee88c-b595-41ea-b4cf-659c01e57101

The Go template rendered a hardcoded Umami script tag:

{{if .UmamiWebsiteID}}
<script defer src="/script.js"
  data-website-id="{{.UmamiWebsiteID}}"
  nonce="{{.Nonce}}"></script>
{{end}}

This worked. But it only worked for Umami. If a self-hoster wanted to use Plausible, Matomo, Simple Analytics, or Fathom, they were out of luck. We’d coupled the app to a specific provider for no good reason.

The fix: accept any <script> tag

We replaced the Umami-specific variable with a generic one:

ANALYTICS_SCRIPT='<script defer src="/script.js" data-website-id="xxx"></script>'

The value is a complete <script> tag. Whatever your analytics provider gives you to paste into your HTML, you paste it here. Umami, Plausible, Matomo — they all work the same way: a script tag with a src and some data attributes.

The Go code reads the env var and renders it verbatim in the watch page:

type watchPageData struct {
    // ...
    AnalyticsScript template.HTML
}

Using template.HTML instead of string tells Go’s template engine to render the value as raw HTML, not escape it. This is safe because the value comes from a server environment variable — it’s set by the operator, not by end users.

The CSP nonce problem

There’s a catch. SendRec uses Content Security Policy with nonces. Every <script> tag needs a nonce attribute that matches the CSP header for that request, or the browser blocks it.

The operator’s script tag won’t have a nonce — they don’t know what nonce the server will generate for each request. So we inject it:

func injectScriptNonce(scriptTag, nonce string) template.HTML {
    if scriptTag == "" {
        return ""
    }
    injected := strings.Replace(scriptTag,
        "<script",
        `<script nonce="`+nonce+`"`,
        1)
    return template.HTML(injected)
}

Simple string replacement. Find <script, insert nonce="..." after it. The 1 limits it to the first occurrence — if someone’s analytics snippet somehow has multiple script tags, only the first gets the nonce (the second would need to be loaded by the first).

Each request generates a fresh nonce, so the injected value is always current:

AnalyticsScript: injectScriptNonce(h.analyticsScript, nonce),

What different providers look like

The same env var works for all of these:

Umami (self-hosted, proxied through your domain):

ANALYTICS_SCRIPT='<script defer src="/script.js" data-website-id="your-id"></script>'

Plausible (cloud or self-hosted):

ANALYTICS_SCRIPT='<script defer data-domain="videos.example.com" src="https://plausible.io/js/script.js"></script>'

Matomo:

ANALYTICS_SCRIPT='<script src="https://analytics.example.com/matomo.js"></script>'

No code changes, no rebuilds, no new env vars per provider. One variable, any provider.

The deploy script that broke

This feature shipped with a bug that broke both staging and production deploys. The .env file on our server had:

ANALYTICS_SCRIPT=<script defer src="/script.js" data-website-id="xxx"></script>

Notice the missing quotes. Our deploy script sources the .env file with source .env, and bash interpreted <script as a redirect operator. The error:

/opt/sendrec/.env: line 19: syntax error near unexpected token `<'

The fix is single quotes:

ANALYTICS_SCRIPT='<script defer src="/script.js" data-website-id="xxx"></script>'

Single quotes prevent bash from interpreting the contents. The value gets loaded verbatim, angle brackets and all.

The same quoting matters in docker run -e arguments. We switched from double quotes to single quotes there too:

docker run -e 'ANALYTICS_SCRIPT=<script ...></script>' ...

A reminder that HTML in shell variables is a quoting minefield, and that even a straightforward change deserves a deploy test.

No analytics by default

When ANALYTICS_SCRIPT is empty or unset, nothing renders. No tracking pixel, no script tag, no network request. Self-hosters get zero analytics tracking out of the box.

This is a deliberate choice. Analytics should be opt-in, not opt-out. If you want tracking on your watch pages, set the variable. If you don’t, your viewers’ browsers stay quiet.

The full change

The diff is small — six files, 139 additions, 11 deletions:

  • video.go: analyticsScript field replaces umamiWebsiteID
  • watch_page.go: injectScriptNonce() helper, template.HTML rendering
  • server.go and main.go: wiring the new env var through
  • watch_page_test.go: three new tests (nonce injection, script rendered, no script when empty)
  • SELF-HOSTING.md: updated docs

No migrations, no database changes, no frontend changes. The analytics script only affects the server-rendered watch page.

Try it

SendRec is open source (AGPL-3.0) and self-hostable. Set ANALYTICS_SCRIPT to your provider’s snippet and every shared video page gets tracking — with the CSP nonce handled for you. The implementation is in watch_page.go.