takt

Node.js (server)

The @vskstudio/takt-core/server entry of the @vskstudio/takt-core core (since 0.9.0) sends pageviews and events from Node, server to server (S2S). It covers what the browser never sees: visitors without JavaScript, crawlers, a payment webhook or a queued job.

The client applies the same payload rules as the browser SDK: event name, props, revenue and URL scrubbing go through the same code. No dependencies, it relies on the native fetch of Node 18+.

pnpm add @vskstudio/takt-core
# or: npm install @vskstudio/takt-core
# or: yarn add @vskstudio/takt-core
# or: bun add @vskstudio/takt-core
Never import @vskstudio/takt-core/server in code shipped to the browser: the API key would be exposed there. On the client, keep init() or a framework wrapper.

createServerTakt()

Create the client once, when the server starts, then reuse it:

import { createServerTakt } from '@vskstudio/takt-core/server'

export const takt = createServerTakt({
  domain: 'example.com',
  apiKey: process.env.TAKT_API_KEY
})
OptionTypeDefaultRole
domainstringrequiredSite identifier, identical to the domain of the site the API key is bound to. When empty, createServerTakt() throws.
apiKeystringnoneSite API key carrying the events:write permission, sent as Authorization: Bearer
endpointstringhttps://taktlytics.com/api/eventFull ingestion URL. Wins over scriptOrigin.
scriptOriginstringhttps://taktlytics.comOrigin to derive the endpoint from (the origin followed by /api/event), for example your first-party proxy
trackQuerybooleanfalseKeeps the query string and hash of url and referrer
queryParamsstring[]noneAllowlist of query parameters to keep
scrubUrl(url: string) => stringnoneCustom URL scrubbing, takes precedence over trackQuery / queryParams
redactRoutesstring[]noneSensitive route patterns, same rules as the browser SDK: a matching path is sent as the pattern (since 0.10.0, see Redacting routes)
strictbooleanfalseThrows on a network error or on any answer other than 202
fetchtypeof fetchglobal fetchfetch implementation to use (tests, custom HTTP agent)

endpoint and scriptOrigin resolve exactly as in createTakt(): endpoint is a full URL, not an origin. The PHP client, for its part, also accepts the bare origin and appends /api/event to it.

pageview() and event()

Both methods return a Promise<void>:

interface ServerVisitor {
  ip?: string
  userAgent?: string
}

pageview(options?: {
  url?: string
  referrer?: string
  route?: string
  visitor?: ServerVisitor
}): Promise<void>

event(name: string, options?: {
  props?: Record<string, unknown>
  revenue?: { amount: string; currency: string }
  url?: string
  referrer?: string
  route?: string
  visitor?: ServerVisitor
}): Promise<void>

A server-rendered pageview, with Express:

import express from 'express'
import { takt } from './takt'

const app = express()

app.get('/pricing', async (req, res) => {
  await takt.pageview({
    url: `https://example.com${req.originalUrl}`,
    referrer: req.get('referer'),
    visitor: { ip: req.ip, userAgent: req.get('user-agent') }
  })
  res.render('pricing')
})

A purchase confirmed by a payment webhook, outside any visitor request:

await takt.event('Purchase', {
  props: { plan: 'pro' },
  revenue: { amount: '29.00', currency: 'EUR' },
  url: 'https://example.com/thanks'
})

Redacting routes

As in the browser, the path of url is sent as is: /verify/abc123 leaks the token. Since @vskstudio/takt-core 0.10.0, two levers replace it with the route pattern (see Route redaction).

redactRoutes applies to every call of the client. A path matching one of the patterns is sent as the pattern, the others keep their real path; the same-site referrer follows the same rule:

export const takt = createServerTakt({
  domain: 'example.com',
  apiKey: process.env.TAKT_API_KEY,
  redactRoutes: ['/verify/[token]', '/reset/:code']
})

The route option of pageview() and event() replaces the path of that call with the given template. Your server already knows which route answered, so pass it as is:

app.get('/invoices/:id', async (req, res) => {
  await takt.pageview({
    url: `https://example.com${req.originalUrl}`,
    route: '/invoices/:id',
    visitor: { ip: req.ip, userAgent: req.get('user-agent') }
  })
  res.render('invoice')
})

The URL sent becomes https://example.com/invoices/:id. With route, a same-site referrer is reduced to the origin, since its template is unknown. Without route, redactRoutes applies.

What the client does for you

  • Authentication: apiKey is sent as Authorization: Bearer. The key must carry the events:write permission and be bound to the site whose domain matches domain exactly, otherwise the ingest answers 401. Without a key, the send takes the same path as a browser, and it is silently dropped if the site requires an API key.
  • Visitor: visitor.ip is sent as X-Forwarded-For and visitor.userAgent as User-Agent. Line breaks (CR/LF) are stripped from header values.
  • URL: without url, the event is attached to the site home (https://example.com/ for domain: 'example.com'), since the ingest rejects an event without an absolute URL. By default, the query string and hash of url and referrer are removed; trackQuery, queryParams and scrubUrl tune this as in the browser.
  • Payload: props are coerced to strings and capped (30 keys, 64-character keys, 1024-character values). A malformed revenue is dropped with a warning, and the event is still sent.
  • Names: an empty name or the reserved pageview name passed to event() always throws, even without strict. Use pageview() for pageviews.
  • Errors: by default, a network error or an answer other than 202 is swallowed, since analytics must never break your server. With strict: true, the promise rejects (handy in tests or in a job that should retry).
Visitor attribution is derived server-side from the forwarded IP and User-Agent. A request authenticated with an `events:write` key is trusted: the ingest computes the visitor and the country from the forwarded IP (the `X-Takt-Client-IP` header, otherwise the first `X-Forwarded-For` entry), not from your application server's IP. Without `visitor.ip`, every event from the same instance is attributed to your server: forward the IP and User-Agent of the current request.

A 202 means “accepted”, not “recorded”: the ingest still filters crawlers. An AI crawler User-Agent forwarded through visitor.userAgent feeds the AI visibility block without counting as a visit.

Source on GitHub: github.com/taktlytics/takt-core · package on npm: @vskstudio/takt-core.