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 @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
}) | Option | Type | Default | Role |
|---|---|---|---|
domain | string | required | Site identifier, identical to the domain of the site the API key is bound to. When empty, createServerTakt() throws. |
apiKey | string | none | Site API key carrying the events:write permission, sent as Authorization: Bearer |
endpoint | string | https://taktlytics.com/api/event | Full ingestion URL. Wins over scriptOrigin. |
scriptOrigin | string | https://taktlytics.com | Origin to derive the endpoint from (the origin followed by /api/event), for example your first-party proxy |
trackQuery | boolean | false | Keeps the query string and hash of url and referrer |
queryParams | string[] | none | Allowlist of query parameters to keep |
scrubUrl | (url: string) => string | none | Custom URL scrubbing, takes precedence over trackQuery / queryParams |
redactRoutes | string[] | none | Sensitive route patterns, same rules as the browser SDK: a matching path is sent as the pattern (since 0.10.0, see Redacting routes) |
strict | boolean | false | Throws on a network error or on any answer other than 202 |
fetch | typeof fetch | global fetch | fetch 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:
apiKeyis sent asAuthorization: Bearer. The key must carry theevents:writepermission and be bound to the site whose domain matchesdomainexactly, otherwise the ingest answers401. 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.ipis sent asX-Forwarded-Forandvisitor.userAgentasUser-Agent. Line breaks (CR/LF) are stripped from header values. - URL: without
url, the event is attached to the site home (https://example.com/fordomain: 'example.com'), since the ingest rejects an event without an absolute URL. By default, the query string and hash ofurlandreferrerare removed;trackQuery,queryParamsandscrubUrltune this as in the browser. - Payload:
propsare coerced to strings and capped (30 keys, 64-character keys, 1024-character values). A malformedrevenueis dropped with a warning, and the event is still sent. - Names: an empty name or the reserved
pageviewname passed toevent()always throws, even withoutstrict. Usepageview()for pageviews. - Errors: by default, a network error or an answer other than
202is swallowed, since analytics must never break your server. Withstrict: true, the promise rejects (handy in tests or in a job that should retry).
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.
@vskstudio/takt-core.