Skip to content

Web analytics

Launch’s analytics script counts visits to your own site and tells you where they came from, what they read, and which of them later signed up or paid. It is one line in your page’s <head>.

Open Insights in the dashboard. The setup card shows your script tag with your site id filled in and asks which domains may send visits:

<script defer src="https://e.nordva.dev/v1/s.js" data-site="ste_…"></script>
  • Next.js: put it in app/layout.tsx, inside <head>.
  • Lovable, Bolt, v0, Replit: ask the AI to “add this script tag to the head of every page”.

The card says “Your first visit is here.” as soon as a visit arrives, usually within a minute. Only the domains you list can send visits for your site: list as many as you need, up to 20, separated by commas (your site, an app on another domain, a demo). Subdomains are included, so example.com covers www.example.com and app.example.com.

Some ad blockers block analytics hosts. To keep those visits, serve the script from your own domain through your server.

If your site sends a Content-Security-Policy header, allow e.nordva.dev in script-src and connect-src, or serve the script from your own domain, which needs neither: Content Security Policy.

The mode decides whether a visit may carry a visitor id, which is what makes unique visitors, bounce rate, funnels and tracing a payment back to a first visit possible. Change it under Insights → Script and tracking mode; it applies within a minute.

ModeWhat it does
Auto (recommended, the default for new sites)Recognises returning visitors outside the EEA, UK and Switzerland. Inside them, only after your cookie banner says yes: call nordva('consent', true) there
Only after consentNobody is recognised until they say yes to your cookie banner. Everyone is still counted, without a cookie
AlwaysRecognises every returning visitor. For when your consent tool loads the script only after a yes, or you need no consent
No cookiesNo visitor is given an id. Visits are counted; unique visitors, bounce rate and first-touch tracking are not available. Add data-cookieless to the tag so the script also reads nothing

A browser sending Global Privacy Control counts as a no. Until the server allows ids for a visitor, the script stores nothing. The Cookie Policy lists every cookie per mode; your own cookie notice should name them too.

Hook your own consent banner up like this:

nordva('consent', true); // on yes
nordva('consent', false); // on no: the script's cookies are removed

If you have no banner of your own, add data-banner to the tag and Launch shows a small one:

<script defer src="https://e.nordva.dev/v1/s.js" data-site="ste_…" data-banner></script>

It appears only when an answer would change what is kept: inside the EEA, the UK and Switzerland in the auto mode, everywhere in “only after consent”. A visitor who already answered, or whose browser sends Global Privacy Control, never sees it. Nothing is stored while it is on screen; “yes” keeps the visitor id from that moment, and “no” is remembered for 180 days in nv_consent.

Change the words with attributes:

AttributeDefault
data-banner-text”We count visits to improve this site. May we keep a cookie so we recognise you when you come back?”
data-banner-accept”Yes, that’s fine”
data-banner-decline”No thanks”
data-banner-linknone; a link to your own policy, added after the text
data-banner-link-text”More”

It sits at the bottom of the page, in its own shadow root, so your styles and its styles cannot collide, and it follows the visitor’s light or dark setting. Its styles are applied in a way a strict Content Security Policy allows.

This banner covers Launch’s own cookies. If your site sets other cookies that need consent, use your own banner and call nordva('consent', …) from it.

For a site in the No cookies mode:

<script defer src="https://e.nordva.dev/v1/s.js" data-site="ste_…" data-cookieless></script>

The mode is kept on Launch’s server, so without the attribute the script learns it from the answer to its first request. Until then it looks for its own cookies and for nordva:ignore and, unless the visitor already said no, sends a random id made in memory, which is not stored when the answer is no. With the attribute the script knows from its first line:

With data-cookieless
Cookies, local storage, session storageNever read, never written
Visitor id, session idNever made, never sent
The question to the server whether ids may be keptNever asked
nordva('consent', …), data-bannerDo nothing
nordva('identify', …)Sends nothing
A submitted data-nordva-identify formSends the address typed, with no id
?nordva_ignore=1Leaves out that page load only

Page views, visits, sources, goals and time on page are counted as in the No cookies mode.

The attribute can only make the script stricter than the mode, never looser. Set the mode to No cookies as well: under any other mode the attribute still wins and no visitor gets an id.

Cookies the script set under an earlier mode are not read, so they are not removed, and run out by themselves (nv_vid and nv_ft after 365 days, nv_consent after 180). To clear them sooner, run the No cookies mode without the attribute for a while first: the script then removes nv_vid, nv_sid and nv_ft on each visitor’s next visit.

The page path (never the query string), the referring site’s hostname, UTM values and ?ref= on the first page, country, region and city, and the browser, operating system and device type. Never stored: the IP address, the full user agent, or any other query value. Map points are rounded to about 11 km. On a computer, for a visit that may carry ids, the first page also makes a keyed one-way code from the IP address, the browser string and the language, kept 30 days after it was last seen and different on every site: a browser that cleared its cookies gets its old visitor id back (see below).

PlanHistory
Free30 days
Indie1 year
Builder2 years

Raw events are kept 90 days. Every finished day (UTC) is also summed into daily totals, which is what longer periods read, so the last 90 days are always exact and older days are exact where a daily total can be:

Last 90 daysOlder days
Visits, page views, goal completions, time on pageexactexact per day
Visitorspeople in the periodeach day’s visitors added up (someone who came on three days counts three times); Insights shows visits instead
Sources, pages, places, devicesevery valueeach day’s top values (100 pages and countries, 50 referrers and campaigns, 200 map places), the rest as one “(other)” row
Signups and goals by sourceany groupingby channel, referrer, UTM source, medium or campaign, or country
Filters, visitor lists, one visitor’s visits, funnels, hourly chartsyesno: these follow single visits, and answer ANALYTICS_RAW_EVENTS_ONLY beyond 90 days

An answer that used daily totals says so: from_rollups: true, and rollups gives the dates and how many of them are not summed yet. The daily totals hold only counts per day, page, source, place and goal: no visitor id, no session and nothing about a person.

Open your site once with ?nordva_ignore=1 in each browser you use, on each device: the script stores nordva:ignore in that browser’s local storage for your site and from then on sends nothing and sets no cookie there. ?nordva_ignore=0 undoes it. Insights has the links under Script and tracking mode. The note is kept by your site, not by Launch, so it cannot be set from the dashboard.

With data-cookieless on the tag the script keeps and reads no note: ?nordva_ignore=1 leaves out only the page load it is on. Leave out your country instead.

To leave a whole country out (your own, while you test), add it on Insights (+ Exclude a country), with excluded_countries in PATCH /v1/analytics/settings, or ask your agent (update_analytics_settings):

Terminal window
curl -X PATCH https://api.nordva.dev/v1/analytics/settings \
-H "Authorization: Bearer $NORDVA_SECRET_KEY" -H "Content-Type: application/json" \
-d '{"excluded_countries": ["NO", "CH"]}'

Every analytics read then leaves them out, including the API, MCP, exports, the weekly digest and the monthly report, and names the list in excluded_countries. One read can count differently with exclude_country=SE or exclude_country= (every country). Nothing is deleted: clear the list and those visits come back. Past 90 days the daily totals know each country’s visits, page views and goals, so totals, charts, goals, the map and the country list leave them out; engaged time and bounce are reduced in proportion (rollups.excluded_estimated) and visitors show as unknown. A breakdown by anything else past 90 days answers ANALYTICS_RAW_EVENTS_ONLY when those countries had visits then.

A goal is an action you name, and it is counted only from the moment you name it: a click last week cannot be added later. So name them when you add the script, not once you wonder.

Tag the element and every click counts, no JavaScript needed:

<button data-nordva-goal="checkout-start">Subscribe</button>

Or from code, for things that are not a click: nordva('goal', 'checkout-start').

Worth naming in most products: a demo or “try it” link, each plan’s button on the pricing page, the start of checkout, the first real action inside the product (the thing the product is for), an invite or a share, and copying an install command. Signing up and logging in have attributes of their own, below.

Every goal carries the source of the visit that completed it: this page load’s referrer and UTM values, or the first touch the visitor arrived with. So a goal can be filtered and grouped like anything else (filter_channel=search, dimension=event).

Names are lower case with hyphens (signup, start-free, demo-open, checkout-start, first-project) and never change: a new name starts a new history. If you use an AI coding agent, ask it to go through the routes and tag them in one pass; with the MCP server connected it can list what is already counted with get_goals. Insights lists each goal with its completions and the share of visitors who reached it; GET /v1/analytics/goals and the MCP tool get_goals return the same.

Most products sign people up on their own site, not through a Launch waitlist. Those two actions have attributes of their own, and they are what the Signups number on Insights counts:

<button data-nordva-signup>Create account</button>
<a data-nordva-signin href="/login">Log in</a>

They send the goals signup and signin. Nothing has to be set up first.

On a form, put the attribute on the <form>: only the submit counts, so a click in a field is not a signup.

<form data-nordva-signup action="/register" method="post"> … </form>

An attribute counts a click or a submit, not a confirmation from your server. If people sign up through OAuth or a magic link, call it on the page they land on afterwards instead:

nordva('goal', 'signup');

Coming back from the login page (Discord, Google, GitHub) or from a Stripe checkout does not make that provider the source. With cookies, a visitor who returns within 30 minutes of their last page is still on the same visit, so the signup keeps the source the visit started with; only a campaign link (UTM tags or an ad click) starts a new one. Without cookies there is no visit to continue, so Launch leaves out the referrers that are only ever sign-in or checkout pages (such as accounts.google.com, appleid.apple.com, checkout.stripe.com and Clerk’s and Auth0’s hosts) and your own domains. A provider that is also a place people find you, like Discord or GitHub, cannot be told apart from a real visit without cookies, and still counts as one.

If your site already sends a goal of its own for this, you do not have to rename it. These names are recognised as they are: signup, sign-up, sign_up, register, signup-complete, and signin, sign-in, login, log-in, sign_in. Anything else you name yourself, in Insights → Signups in your product (“Change”), or with PATCH /v1/analytics/settings and { "signup_goal": "joined" }. Only one name is ever counted, and the card always says which one.

Insights then shows signups and sign-ins for the period, the share of new visitors who signed up, and where those signups came from. GET /v1/analytics/signups returns the same.

Two things to know. Without cookies (the “No cookies” mode, or a visitor who said no) a signup is credited to a source only when it happens on the page the visit started, because there is no first touch to fall back on. And every sign-in is one event against your monthly quota: 1,000 sign-ins a day is 30,000 events a month, where Free includes 10,000 and Indie 100,000.

Anywhere somebody types an email on your site, that is the same fact as an identify() call: this visitor is that person. Put the attribute on the form and the script does it when the form is submitted.

<form data-nordva-identify action="/contact" method="post">
<input type="email" name="email">
<textarea name="message"></textarea>
<button>Send</button>
</form>

It reads the email field. If the form has more than one, or the address is not in a type="email" input, name the field:

<form data-nordva-identify="work_email"> … </form>

This works on a contact form, a newsletter box, a login form, a checkout, or anything else you already have. From then on that person’s earlier visits are on their contact page, and a later payment traces back to the first visit.

A submitted form is sent even when the visit may not carry ids (the “Ask first” modes before anyone accepts, or Global Privacy Control), because the person gave you the address by submitting it. Then it goes without a visitor id: the person becomes a contact with what the form sent, and their visits are not linked to them.

Two things it does not do. It reads only the address, only from the form being submitted, and sends nothing else in the form unless you mark a field (below). And it can only link visits that carried a visitor id: in the “Ask first” modes, a visit where nobody accepted carried none, so there is nothing to attach a person to, then or later.

Mark a field with data-nordva-detail="name" and its value is kept on the person’s contact as a detail; mark a checkbox with data-nordva-consent="purpose" and, when it is ticked, that consent is recorded as granted. An unticked box records nothing.

<form data-nordva-identify>
<input type="email" name="email">
<select name="building" data-nordva-detail="building">
<option value="saas">A SaaS product</option>
<option value="mobile_app">A mobile app</option>
</select>
<label><input type="checkbox" data-nordva-consent="email_marketing"> Email me product news</label>
<button>Sign up</button>
</form>

In code it is nordva('identify', { email, details: { building: 'saas' }, consents: { email_marketing: true } }), or details and consents in the body of POST /v1/analytics/identify. The naming rules, the limits, what to record as consent and how to export a list are in Contacts.

Some people set their browser to delete everything when it closes, or use one that does it for them. Each time they come back they arrive with no cookie at all.

On a computer, Launch recognises most of them anyway. On the first page of a visit that may carry ids, the collector makes a keyed one-way code from the IP address, the full browser string and the browser language. If the same code was seen on your site in the last 30 days, the browser gets its old visitor id back, and the visit counts as a returning visitor instead of a new one. The address is never stored and cannot be read back from the code, and the code is different on every site. Three rules keep strangers apart:

  • Phones are never matched: many share one mobile-network address and an identical browser string.
  • A code seen with two browsers that both kept their cookies (an office, a VPN exit) is never used to match again.
  • Nothing is matched for a visit that may not carry ids: a visitor in the EEA who has not said yes, or a browser sending Global Privacy Control.

A new address (another network, a VPN switching exits) or a browser update makes a new code, so the match is a help, not a guarantee.

What always works is them telling you who they are. Every identify() links that visit’s id to the person, and a person can have any number of ids, so call it whenever a signed-in page of your app loads (once when a single-page app starts), not only at login. Each day they sign in, that day joins their contact page, and their source stays the one from their first visit. A form with data-nordva-identify does the same for people who never sign in.

Insights → Visitors says how many of the people you identified came on more than one id (another device counts too), so you can see how far “visitors” overstates people on your site.

A contact’s source is read from the visit: the UTM tag, else the referring site, else “direct”. Some people come in where no visit can say: your app, an import, a partner’s form, a sales call. Name it yourself, and the contact shows that name when the visit says nothing:

nordva('identify', { email, source: 'ios-app' });
<form data-nordva-identify data-nordva-source="partner-form"> … </form>

From your server it is source in the body of POST /v1/analytics/identify. A source is any short tag, lowercased, with nothing to create first, and the first one named for a person is kept. A source read from the visit still wins, so naming one on an ordinary web signup changes nothing. Filter Contacts by it like any other source.

A funnel is a path people should take, such as home → pricing → signup. Each step is a page path or a goal name. Create one under Insights → Where visitors drop off, with POST /v1/analytics/funnels, or ask your AI agent (create_funnel). Free has 1 funnel of 3 steps, Indie 5 of 5, Builder 25 of 8.

The report follows visitors with an id through the steps in order and shows where they drop off, over up to 90 days (it needs raw events). When visits carry no id (the “No cookies” mode, or no consent), it shows each step’s total instead and says so.

Launch links a payment to the visit that started it, in this order:

  1. nordva_visitor_id in the Stripe customer’s metadata, or in the subscription’s: with Checkout, pass subscription_data: { metadata: { nordva_visitor_id } }. Checkout Session metadata itself stays on the session and is not read.
  2. nordva('identify', { user_id, email, source }) in the browser after a signup or login, or POST /v1/analytics/identify from your server. A user_id matches the Stripe customer whose metadata carries it under the key you chose when connecting Stripe. The email is stored with that person and also as a hash, which is what matches them to a Stripe customer. Anyone you identify this way appears under Contacts, next to your waitlist signups, feedback and paying customers, so a product with its own sign-up and no waitlist still has one page per person.
  3. The customer’s email on your waitlist. A signup through a Launch form on a page running the script carries its visitor id, so the first visit counts; otherwise the signup’s own source does.

In the browser, nordva.visitor()?.id gives the visitor id (null until the visit may carry ids); on your server it is the nv_vid cookie’s value before the dot. With Stripe connected, Insights → Where paying customers came from shows how many paying customers were traced, and revenue per source and per country. On Builder, a contact’s page shows the visits before they signed up.

Every read has an MCP tool: get_analytics_summary, get_analytics_breakdown, get_live_visitors, get_goals, get_funnel_report and more. Ask “where did this week’s visitors come from?” and the agent answers from your numbers. See MCP setup.