Documentation

Using Tastatur

One script tag installs it. Everything below is optional.

Install

Paste this into your <head>. That is the entire installation, and nothing below is required to get numbers.

HTML
<script defer data-site="YOUR_SITE_KEY" src="https://tastatur.dev/t.js"></script>

defer keeps it off the critical path. The script is about 4.1 KB over the wire and makes one request per pageview. Visits from localhost, private network ranges and file: are ignored, so local development will not appear in your numbers.

Script options

Attributes on the script tag. All optional.

data-site required

Your site key. Public by design; it appears in your page source.

data-api optional

Override the ingest endpoint. Only needed if you proxy events through a different path than the script.

data-hash optional

Count URL fragment changes as pageviews. For apps that route on #.

data-auto default "true"

Set to "false" to stop automatic pageviews and send them yourself.

data-exclude optional

Present means collect nothing. Useful on a staging build from the same codebase.

data-ignore-dnt optional

Stop honouring Do Not Track and Global Privacy Control. Read the note under Opt-out before using this.

HTML
<script defer
  data-site="YOUR_SITE_KEY"
  data-hash
  src="https://tastatur.dev/t.js"></script>

Custom events

Anything worth counting that is not a pageview: a signup, a download, a plan selected.

JavaScript
tastatur('event', 'Signup')

tastatur('event', 'Signup', { props: { plan: 'pro', source: 'pricing-page' } })

Event names are free text up to 120 characters. Keep them stable; renaming one starts a new series rather than renaming the old.

Properties are limited to 24 keys per event, 60 characters per key and 500 per value. Values may be strings, numbers or booleans.

Do not put personal data in properties. This is the one field whose contents you control, and nothing in Tastatur can tell that { email: "…" } should not have been sent.

Where they show up. Custom events get their own panel on the dashboard. Click one and the whole dashboard scopes to it, with a card per property key listing its top values — click one of those to narrow further. Properties are only shown against a chosen event, because plan=pro on a signup and plan=pro on a cancellation are not the same fact and a pooled total of the two means nothing. Property rows are held to the same k-anonymity threshold as every other breakdown.

If you call tastatur() from code that may run before the deferred script has loaded, add this stub first. It queues calls and replays them once the script is ready.

JavaScript
window.tastatur = window.tastatur || function () {
  (window.tastatur.q = window.tastatur.q || []).push(arguments)
}

Revenue

Attach a monetary value to an event and it shows up against your goals.

JavaScript
tastatur('event', 'Purchase', {
  revenue: { amount: 4900, currency: 'EUR' },
  props:   { plan: 'growth' }
})

amount is in minor units, so 4900 is €49.00. A currency is required whenever an amount is sent: without one, a total would silently mix euros and yen.

Revenue attribution

Not shipped yet. Connect Stripe and see which channel produced paying customers, not just visitors. Three steps, and the middle one is the only code you write.

This is not available yet. The shapes below are settled and the endpoints are not open. Hear when it ships.

Why it needs your help. The browser knows where somebody came from. The payment happens somewhere else, possibly days later, possibly on another device. We store nothing on the visitor's device, so there is no cookie to join those two together — instead the attribution travels with the payment, inside it.

1. Capture it on the landing page. This reads the current URL's campaign tags and referrer. Store it in your own user record or form state — calling it at the end of a signup funnel would read your own funnel as the source.

JavaScript
const attribution = tastatur.attribution()
// { source, medium, campaign, landing_path, referrer_host, first_seen_at }

2. Send it when they sign up, from your server, with an API key from your site's settings. The email is hashed on receipt and never stored.

HTTP
POST https://tastatur.dev/api/v1/identify
Authorization: Bearer tk_...
Content-Type: application/json

{
  "external_id": "user_9182",
  "email": "[email protected]",
  "attribution": { "source": "reddit", "medium": "social", "landing_path": "/pricing" }
}

3. Attach it to Stripe Checkout. This is what makes attribution survive a closed tab or a payment finished three days later on a phone.

JavaScript
stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [...],
  client_reference_id: user.id,
  metadata: tastatur.checkoutMetadata(attribution)
})

checkoutMetadata() returns a flat object with tst_-prefixed keys, so it cannot collide with metadata of your own. Call it with no argument to use the current page, or pass a first touch you saved earlier. If you build the object yourself, the keys are tst_source, tst_medium, tst_campaign, tst_content, tst_term, tst_landing_path, tst_referrer_host and tst_first_seen_at.

First touch wins, and is never overwritten. Apps call identify on every sign-in, so last-write-wins would re-attribute a customer you earned from a Reddit post to whatever they searched months later — every paid channel would decay towards zero on its own.

Attribution is not anonymous, and is kept separately. Everything on this page above it is. These endpoints carry data your own application already holds about somebody who signed up for it, sent server-to-server under your own legal basis. Nothing here is ever linked back to an anonymous visitor.

Single-page apps

Handled automatically. There is nothing to configure for React, Vue, Svelte, Turbo, HTMX or hand-rolled routing.

The script patches history.pushState and replaceState, and listens for popstate, so client-side navigation counts as a pageview.

Views are deduplicated against the last URL sent. Frameworks that call replaceState during hydration would otherwise double-count every first pageview.

Back/forward cache restores are counted via pageshow, because a restored page does not re-run the script.

If you route on the URL fragment, add data-hash.

To take control yourself, set data-auto="false" and call it from your router:

JavaScript
router.afterEach(() => tastatur('pageview'))

Goals

A goal is a page or an event worth counting, with a conversion rate attached.

Exact path /pricing Matches that page only.
Starts with /blog Matches /blog and everything beneath it.
Wildcard /blog/** * matches inside one path segment, ** matches across segments.
Custom event Signup Matches an event name you send.
Goals are matched against history, so creating one immediately reports on data you already have. You do not need to set them up in advance.

Funnels

An ordered sequence of steps, with the drop-off between each.

Steps use the same matchers as goals. A visitor counts at step 3 only if they reached it after step 2, which means someone who backtracks and returns is still counted correctly.

Pick a window of 24 hours or less. Visitor identifiers expire daily, so a funnel spanning more than a day cannot follow anyone across that boundary and will undercount. The funnel view says so when the window is longer, rather than quietly reporting a low number.

Campaigns

Name what you are promoting, build a link for each place you post it.

A campaign link is an ordinary URL to one of your own pages with utm_source, utm_medium and utm_campaign on it. Nothing needs installing — the tracker already reads those five parameters. Building the link here means the report can match it back afterwards, which is the part that goes wrong when links are written by hand.

A campaign is a name and a tag, and you build a link under it for each place you post — Discord, Facebook, the newsletter. They all carry the campaign's tag, which is what puts them under one heading with one figure as well as one row each.

That heading figure is counted over the tag rather than added up from the links, so it is usually smaller than they come to: somebody who follows two of your links is one visitor to the campaign and a visitor on each link. Adding the rows together would count them twice.

Matching is exact and case-sensitive, so Newsletter and newsletter are two campaigns. Values you build here are lower-cased and the pickers offer what the site has already used.

Tagged traffic from links built anywhere else — an ad platform, a mail tool — is measured too, and listed separately so a spelling that should have matched one of yours is visible.

A value that reads like a token is discarded on arrival, because mail platforms put per-recipient identifiers in utm_content. The builder refuses those rather than saving a link that would report nothing.

Filtering and sharing

Click any row on the dashboard to filter everything by it.

Filter state lives in the URL, so a filtered view can be bookmarked, sent to a colleague, or kept open in a tab.

Filterable dimensions: page, entry page, source, referrer, country, browser, operating system, device, screen class, every utm_* field, event name and hostname.

Shared dashboards give a read-only link that needs no account, optionally password-protected and optionally expiring. They do not offer filters, because narrow filters on someone else's audience are a re-identification tool.

Content Security Policy

If your site sets a CSP, allow this origin in two directives.

CSP
script-src https://tastatur.dev;
connect-src https://tastatur.dev;

script-src lets the browser fetch t.js; connect-src lets it send the beacon. The script needs no unsafe-inline, no unsafe-eval, and sets no cookie, so it does not interact with your cookie or frame policy at all.

Server-side events

The ingest endpoint is a plain HTTP API, so backend and mobile events work without the script.

cURL
curl -X POST https://tastatur.dev/api/event \
  -H 'Content-Type: application/json' \
  -H 'X-Forwarded-For: <the end user's IP>' \
  -H 'User-Agent: <the end user's user-agent>' \
  -d '{
        "s": "YOUR_SITE_KEY",
        "u": "https://example.com/checkout",
        "n": "Purchase",
        "v": 4900,
        "c": "EUR"
      }'
Pass the end user's address and user-agent, not your server's. Otherwise every server-side event collapses into one visitor located wherever your server is. This only works from a host your Tastatur instance trusts as a proxy.
s Site key
u Absolute URL of the page or screen
n Event name. Defaults to "pageview"
r Referrer URL
w Viewport width in pixels, bucketed server-side
p Properties object
v Revenue in minor units
c Three-letter currency code

The endpoint answers 202 for everything, including a payload it rejected. That is deliberate: a distinguishable response would let anyone test which site keys are valid. Check your dashboard to confirm events are arriving.

Without JavaScript

A tracking pixel for visitors who run no scripts.

HTML
<noscript>
  <img src="https://tastatur.dev/api/pixel?s=YOUR_SITE_KEY&u=https://example.com/"
       alt="" width="1" height="1" referrerpolicy="no-referrer-when-downgrade">
</noscript>

The u parameter has to be written per page, so this is most practical in a server-rendered layout where you can interpolate the current URL. Everything else behaves the same, including bot filtering and the opt-out check.

Opt-out

Do Not Track and Global Privacy Control are honoured by default.

If a visitor's browser sends DNT: 1 or Sec-GPC: 1, no identifier is computed and no event is stored. Only an anonymous per-hour counter is incremented, so you can see that some requests opted out without any record of who.

This is checked in the script and again server-side, because the endpoint is a public API that an older cached script or the pixel might call without checking.

There is no per-person opt-out flag, because storing one would mean keeping a durable identifier for exactly the people who asked us not to.

data-ignore-dnt turns this off. A working opt-out is a condition of the audience-measurement consent exemptions Tastatur is designed to qualify for, so disabling it likely forfeits that argument. Take your own advice before using it.

Ad blockers

Some block Tastatur. You can proxy it through your own domain, and here is our position on doing so.

The script derives its endpoint from its own src, so proxying the script through your domain automatically proxies the events with it. Point a path on your site at this instance:

nginx
location /stats.js  { proxy_pass https://tastatur.dev/t.js; }
location /stats/api { proxy_pass https://tastatur.dev/api/event; }
HTML
<script defer data-site="YOUR_SITE_KEY" src="/stats.js"></script>

Where we stand on this. Blockers exist because tracking earned them. Tastatur sets no cookie, stores nothing on the device and builds no cross-site profile, so a blocked Tastatur request protects nobody from anything.

That said, proxying is a way around a user's stated choice, and we will not pretend otherwise. It is documented because you will find it anyway and it is better done with the tradeoff in front of you. Note also that Do Not Track and Global Privacy Control are still honoured through a proxy, so a visitor who has explicitly objected remains uncounted.

What gets collected

The short version. The long version is a page of its own.

Page path

Query string stripped except utm_* tags. Personal data is removed from the path itself: emails, tokens, UUIDs and long ids are replaced with placeholders.

Referrer host

news.ycombinator.com, not the full referring URL.

Country

Two letters. No region, no city, no coordinates.

Browser and OS

Family plus major version only.

Device class

One of desktop, mobile, tablet.

Screen class

One of five buckets. The exact pixel width is never stored.

Visitor identifier

A truncated HMAC of a daily-rotating secret with the IP, site, and the coarse browser facts below — never the raw user-agent. Unlinkable once the secret is destroyed.

Raw IP addresses and user-agent strings are never written to the database, a log, or disk. A returning visitor is counted as a new visitor tomorrow, so a 30-day figure is the sum of 30 daily figures rather than a count of distinct people.

Cookies, precisely

On your visitors' devices

Nothing

No cookie is set and none is read. Nothing is written to localStorage, sessionStorage or IndexedDB. No identifier is stored on the device in any form. This is the part that determines whether you need a consent banner for Tastatur, and it is the promise the product is built around.

On this dashboard, for you

Up to three

A first-party session cookie, set when you sign in, so you stay signed in. A second if you tick "stay signed in". A third if you use two-factor authentication and ask us not to send a code to this browser every time. All strictly necessary for a login and exempt from consent on that basis. They are set on our site, for account holders, and never touch the sites you measure or the people who visit them.

We say this explicitly because "no cookies" is used loosely in this category. A login needs a session cookie; pretending otherwise would be nonsense rather than privacy. What matters is that nothing of ours reaches your visitors.