takt

PHP

The PHP core vskstudio/takt-core-php (Packagist, v0.6.0) is framework-agnostic. It covers two server-side needs: rendering the snippet for the browser runtime into your HTML, and sending server-to-server (S2S) events straight from PHP. The Laravel and Symfony bridges build on it.

composer require vskstudio/takt-core-php

PHP 8.1+. SnippetRenderer needs no HTTP dependency: that line is enough to render the snippet.

The S2S client Takt, on the other hand, discovers its transport via PSR-18 / PSR-17 (php-http/discovery), and the package only declares the interfaces — you bring the implementation:

composer require vskstudio/takt-core-php guzzlehttp/guzzle
# or: composer require vskstudio/takt-core-php symfony/http-client nyholm/psr7

With no PSR-18 / PSR-17 implementation installed, new Takt(...) throws at construction time (discovery happens in the constructor, outside the fire-and-forget net).

SnippetRenderer — the browser runtime

SnippetRenderer produces the <script> block to drop into your <head>. It is configured with a readonly Options value object.

use Vskstudio\Takt\SnippetRenderer;
use Vskstudio\Takt\Options;
use Vskstudio\Takt\Mode;

$renderer = new SnippetRenderer(new Options(
    domain: 'example.com',
    outbound: true,
    files: true,
    tagged: true,
    notFound: true,
    fileExtensions: ['pdf', 'zip'],
));

echo $renderer->render(); // place inside <head>
FieldDefaultMeaning
domain(required)Site domain
endpointhttps://taktlytics.com/api/eventIngest endpoint — defaults to the hosted Takt origin so a bare setup works; pass /api/event for a same-origin proxy
scriptOriginnullFirst-party origin to serve the runtime + derive the endpoint (dodge ad-blockers; endpoint wins)
outboundfalseTrack outbound links (outbound token)
filesfalseTrack file downloads (downloads token)
fileExtensions[]Restrict downloads to these extensions (data-downloads-ext); empty keeps the default list
taggedfalseTrack elements marked data-takt-event (tagged token)
notFoundfalseTrack 404 pageviews (404 token)
excludeLocalhosttrueDrop localhost events
noncenullCSP nonce for the <script> tag
sampleRatenullKeep only this fraction (0–1) of hits (data-sample-rate)
trackQuerynullKeep the query string + hash (data-track-query); off strips them
queryParams[]Allowlist of query params to keep (data-query-params) — no effect when trackQuery is on, which keeps everything
respectDntnullfalse stops honoring Do-Not-Track (data-respect-dnt)
enablednullfalse = kill-switch, no-op snippet (data-enabled)
scrubUrlnullRaw JS URL-rewrite function — requires Mode::Sdk
exclude[]Path prefixes never tracked (segment-bounded) — requires Mode::Sdk
redactRoutes[]Sensitive route patterns sent as the pattern, see Route redaction (requires Mode::Sdk)
routeTemplatesfalseSends every page as its route template (requires Mode::Sdk)
routeTemplatenullRoute template of the page being rendered, read when routeTemplates is on
modeMode::InlineRuntime sourcing (below)

enum Mode:

  • Inline — embeds the vendored takt.auto.js bundle inline in the tag (zero extra requests, CSP-friendly with a nonce).
  • Cdn: emits a <script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/[email protected]/dist/takt.auto.js"> loader.
  • Asset — defer src="/takt/takt.auto.js" pointing at a self-hosted copy, or {scriptOrigin}/takt/takt.auto.js when scriptOrigin is set.
  • Sdk — emits a <script type="module">import{init}…;init({…})</script> that boots the full SDK. The only mode able to express scrubUrl; the module is loaded from {scriptOrigin}/takt/takt.esm.js when scriptOrigin is set, otherwise from jsDelivr. The package only vendors takt.auto.js: takt.esm.js is not shipped, it is on you to serve it at that URL, otherwise the module 404s and nothing is measured.

Autocapture is opt-in: outbound, files, tagged and notFound each add a token to a single data-auto attribute the bundled takt.auto.js reads; fileExtensions narrows which downloads count.

Advanced options

Each advanced option defaults to null (“unset” — the tracker’s own default applies); only a non-default value is rendered. sampleRate, trackQuery, queryParams, respectDnt and enabled mirror to data-* attributes in Inline, Cdn and Asset modes (snippet parity, see Configuration); in Mode::Sdk no data-* attribute is emitted, everything goes into the object passed to init(). scrubUrl is a raw JS function injected verbatim into the page: it can only be expressed in Mode::Sdk (passing it in any other mode throws) and stays dev-controlled — never build it from user input. exclude likewise lives only in the full SDK (the ≤ 1 kB minimal snippet omits it): it requires Mode::Sdk and throws in the other modes rather than silently dropping paths you believed were excluded.

new Options(
    domain: 'example.com',
    mode: Mode::Sdk,
    sampleRate: 0.5,
    queryParams: ['utm_source'],
    exclude: ['/app', '/account'],
    scrubUrl: '(u) => u.split("?")[0]',
);
SPA navigation tracking is always on (no spa toggle). Do-Not-Track is honored by default; only pass respectDnt: false (or data-respect-dnt="false") if you know exactly why.

Options::fromArray(array $a): self builds the options from an array and additionally accepts snake_case aliases (exclude_localhost, not_found, file_extensions, sample_rate, track_query, query_params, respect_dnt, scrub_url, redact_routes, route_templates, route_template). That is what makes the Laravel and Symfony bridges config-file driven. Two constants save you from retyping the URLs: Options::HOSTED_ORIGIN (https://taktlytics.com) and Options::HOSTED_ENDPOINT (https://taktlytics.com/api/event). If you assemble an inline tag yourself, SnippetRenderer::neutralizeScriptClose(string $js): string is public: it escapes the script closing tags contained in a bundle.

Route redaction

URL scrubbing removes the query and the hash, but the path is sent as is: /verify/abc123 leaks the token. Since 0.6.0, three options replace those paths with the route pattern, following the browser SDK rules (see Route redaction). They live in the full SDK and require Mode::Sdk: setting redactRoutes or routeTemplates in any other mode makes the SnippetRenderer constructor throw an InvalidArgumentException, since the minimal snippet cannot redact. Mode::Sdk loads @vskstudio/takt-core 0.10.0, the first version that supports these options.

new Options(
    domain: 'example.com',
    mode: Mode::Sdk,
    redactRoutes: ['/verify/{token}', '/reset/:code', '/invoices/[id].pdf'],
);

redactRoutes lists the routes that carry a secret. A path matching one of the patterns is sent as the pattern, every other path keeps its real value. Accepted patterns:

SyntaxExampleMeaning
[p]/verify/[token]One segment
[[p]]/blog/[[page]]Optional segment
[...p]/docs/[...rest]Rest of the path
(group)/(app)/verify/[token]Ignored group
:p, :p?/reset/:codeOne segment, optional with ?
*, **/files/**Segment wildcard, rest of the path
{p}, {p?}/users/{id}Laravel / Symfony syntax, optional with ?

A segment can mix text and a parameter ([id].pdf). The pattern that is sent is normalized: groups are dropped, and so are requirements and defaults ({id<\d+>?1} becomes {id}).

The browser SDK does not read the brace syntax: the renderer hands it {token} as [token] and {page?} as [[page]]. A browser pageview therefore reports /verify/[token], while an event sent by the Takt server client reports /verify/{token}. Write the pattern with brackets if both must land on the same row of the Pages report.

new Options(
    domain: 'example.com',
    mode: Mode::Sdk,
    routeTemplates: true,
    routeTemplate: '/users/{id}',
);

routeTemplates: true sends every page as its route template: /users/42 becomes /users/{id}. PHP renders each page on the server, so pass the template of the current route as routeTemplate: it is emitted as a constant resolver (routeTemplate: () => "/users/{id}"), in canonical form ({id?} becomes {id}). This template is not translated to brackets: with routeTemplates, the browser pageview and the server event both report /users/{id}. Without routeTemplate, redactRoutes still applies and the real path is sent otherwise.

Options::withRouteTemplate(?string $routeTemplate): self and SnippetRenderer::withRouteTemplate(?string $routeTemplate): self return a copy carrying the template of the current request, without rebuilding the whole configuration:

echo $renderer->withRouteTemplate($currentRoute)->render();

The Laravel and Symfony bridges fill that template for you. The WordPress plugin does not expose these options.

Snippet redaction and server client redaction are configured separately: the redactRoutes of Options does not apply to the Takt client, which takes its own list (see Redacting routes in S2S).

Takt — the server-to-server client

Takt posts events to POST /api/event with an API key carrying the events:write permission (takt_ik_ prefix), bound to the site whose domain matches domain exactly — otherwise ingest answers 401. Ideal for actions that don’t happen in the browser (payment webhook, queued job…).

The S2S client’s endpoint accepts the origin (Options::HOSTED_ORIGIN, or that of your first-party proxy) as well as the full collect URL (Options::HOSTED_ENDPOINT): /api/event is only appended when missing, never twice. It must be absolute.

use Vskstudio\Takt\Options;
use Vskstudio\Takt\Revenue;
use Vskstudio\Takt\Takt;

// Forward the real visitor's IP + User-Agent for attribution
// (adapt how you read the request to your framework):
$takt = (new Takt(
    endpoint: Options::HOSTED_ORIGIN, // https://taktlytics.com
    domain: 'example.com',
    apiKey: $_ENV['TAKT_API_KEY'],
))->withVisitor($_SERVER['REMOTE_ADDR'] ?? null, $_SERVER['HTTP_USER_AGENT'] ?? null);

$takt->event('Signup', ['plan' => 'pro'], null, 'https://example.com/signup');

$takt->event(
    'Purchase',
    ['plan' => 'pro'],
    new Revenue(amount: '29.00', currency: 'EUR'),
    'https://example.com/thanks',
);

$takt->pageview('https://example.com/thanks');
  • __construct(string $endpoint, string $domain, ?string $apiKey = null, ?ClientInterface $httpClient = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null, array $redactRoutes = []): $httpClient, $requestFactory and $streamFactory inject the PSR transport (required for testing); left null, they are discovered. $redactRoutes lists the sensitive routes, see Redacting routes in S2S.
  • withVisitor(?string $ip, ?string $userAgent): self — bind the real visitor before sending (returns an instance to send from)
  • event(string $name, array $props = [], ?Revenue $revenue = null, ?string $url = null, ?string $referrer = null, ?string $route = null): void: $props is an array<string,scalar> whose every value is cast to a string (true goes out as '1')
  • pageview(?string $url = null, ?string $referrer = null, ?string $route = null): void
  • withRoute(string|\Closure|null $route): self: default route template for every call of the returned clone
  • strict(): self — returns a clone that throws on a transport error or on any response other than 202 (handy in tests)
  • Fire-and-forget: in default mode, transport errors and error responses alike are swallowed (analytics must never break the app). A 202 means “accepted”, not “recorded”: a User-Agent classified as a bot, or an opt-out (DNT / GPC), is dropped server-side — still with a 202.
Pass $url as an absolute http(s) URL to attach the event to the right page. Left out, the event is attached to the site home page (https:// followed by domain, then /): handy for a hook with no page context (a WooCommerce purchase, a queued job), but all such actions then land on the same page.
Visitor attribution is derived server-side from the IP and User-Agent. Without withVisitor(), S2S events attribute to the application server, not the real visitor — forward the IP / User-Agent from the current request.

Revenue is a readonly value object (amount, currency): the amount is a decimal string, the currency a 3-letter uppercase code (mirroring the JS SDK). Both are validated at construction — '29,00' or 'eur' throw an InvalidArgumentException.

Redacting routes in S2S

The Takt client redacts routes with the same patterns as the snippet (see Route redaction), but it is configured on its own:

  • redactRoutes, passed to the constructor, applies to every call: the page URL and the same-site referrer are sent as the pattern when they match.
  • The route argument of pageview() and event() sends that call under the given template.
  • withRoute() returns a clone whose calls are all sent under a default template. It takes a string, or a closure resolved at send time (a closure that throws or does not return a string is ignored). An explicit route wins over that default.
$takt = new Takt(
    endpoint: Options::HOSTED_ORIGIN,
    domain: 'example.com',
    apiKey: $_ENV['TAKT_API_KEY'],
    redactRoutes: ['/verify/{token}'],
);

$takt->pageview('https://example.com/verify/abc123');

$takt->event('Verified', url: 'https://example.com/users/42', route: '/users/{id}');

$perRequest = $takt->withRoute(fn (): ?string => $router->currentTemplate());

The first URL is sent as https://example.com/verify/{token}, the second as https://example.com/users/{id}. On the server, braces are not translated to brackets. With a template (route or withRoute()), a same-site referrer is reduced to the origin, since its template is unknown.