takt

Symfony

The Symfony bridge vskstudio/takt-symfony exposes the PHP core as a bundle: a Twig function for the snippet and an autowired Takt service for S2S. Supports Symfony 6.4 / 7.x, PHP 8.1+.

composer require vskstudio/takt-symfony

The package ships no Flex recipe: register the bundle yourself.

// config/bundles.php
return [
    // …
    Vskstudio\Takt\Symfony\TaktBundle::class => ['all' => true],
];

TaktBundle then registers its services from semantic config: the SnippetRenderer behind the Twig function, and the Takt S2S client. The latter is RequestStack-aware: it forwards the current request’s IP and User-Agent for attribution.

This page is the option reference. For the end-to-end integration and its traps (snippet placement under Turbo, return controller vs webhook for conversions): see the guide cookieless analytics in a Symfony app.

Configuration

config/packages/takt.yaml:

takt:
    domain: 'example.com'
    endpoint: 'https://taktlytics.com'
    api_key: '%env(TAKT_API_KEY)%'                 # S2S only: omit the key otherwise
    mode: inline
    outbound: true
    files: true
    tagged: true
    not_found: true
    file_extensions: ['pdf', 'zip']

A %env(...)% must match a declared variable: without it, the container throws EnvNotFoundException on every page that renders the snippet.

# .env
TAKT_API_KEY=tk_…

Key reference

KeyDefaultRole
domain''Site domain (data-domain attribute, and the domain carried by every S2S event)
endpointhttps://taktlytics.comIngest target: the origin or the full collect URL, see below
script_originnullFirst-party origin serving the runtime and deriving the endpoint from it (dodges ad-blockers); endpoint wins over it
api_keynullscope=ingest API key, used by the S2S client only
modeinlineRuntime source: inline, cdn, asset or sdk
outboundfalseTracks outbound links (outbound token)
filesfalseTracks downloads (downloads token)
taggedfalseTracks elements marked with data-takt-event (tagged token)
not_foundfalseTracks 404 pages (404 token)
file_extensions[]Restricts which downloads are counted; empty keeps the tracker’s built-in list
exclude_localhosttrueDrops hits fired from localhost, the most common reason nothing shows up in dev
noncenullCSP nonce set on the <script> tag (required in mode: inline under a strict CSP)
redact_routes[]Sensitive routes sent as their pattern, requires mode: sdk (see Route redaction)
route_templatesfalseSends every page as its route path, requires mode: sdk
redact_undecodable_pathstrueUnreadable paths matched against the routes instead of /[redacted]; S2S events only
sample_ratenullSends only a fraction (0 to 1) of hits
track_querynullKeeps the query string + hash (default: stripped)
query_params[]Allowlist of params kept when track_query is off
exclude[]Path prefixes never tracked (requires mode: sdk)
respect_dntnullfalse stops honoring Do-Not-Track
enablednullfalse = kill-switch, no-op snippet
scrub_urlnullRaw JS function rewriting URLs (requires mode: sdk)

The last five advanced options default to null (“unset”: the tracker default applies); only set them for a non-default value: sample_rate: 0.5 throws away half your traffic and enabled: false shuts tracking off entirely.

The modes:

  • inline: embeds the vendored takt.auto.js bundle in the tag (no extra request).
  • cdn: loader <script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/[email protected]/dist/takt.auto.js">.
  • asset: loader pointing at /takt/takt.auto.js, a copy you host yourself (see the callout at the end of this page).
  • sdk: <script type="module"> booting the full SDK.

mode is not validated: an unknown value silently falls back to inline.

The endpoint key

The same key feeds two consumers: the Twig snippet, which needs the full collect URL, and the Takt service (S2S), which needs the origin it appends /api/event to. The bundle therefore accepts both forms and normalises them for each:

endpoint valueThe snippet POSTs toThe S2S service POSTs to
https://taktlytics.com (default)https://taktlytics.com/api/eventhttps://taktlytics.com/api/event
https://taktlytics.com/api/eventhttps://taktlytics.com/api/eventhttps://taktlytics.com/api/event
/api/event (same-origin proxy)/api/event on your domainrelative path: no send goes through

A value carrying any other path is taken verbatim as the collect URL: that is the case for a first-party proxy served from your own domain. A relative path only suits the snippet; if you also send server-side events, keep an absolute URL.

script_origin only derives the snippet endpoint with the hosted endpoint (in either form above): the tracker then POSTs to {script_origin}/api/event. With any other value, endpoint is emitted as data-endpoint and wins.

mode: sdk

mode: sdk loads the SDK as an ES module. It is the only mode able to express scrub_url, a raw JS function rewriting URLs that is injected verbatim into the page, and exclude, the path prefixes never tracked. The minimal snippet carries neither: setting them in another mode throws at render time instead of silently dropping them. Keep scrub_url dev-controlled: never build it from user input.

takt:
    mode: sdk
    exclude: ['/app', '/account']
    scrub_url: "(u) => u.split('#')[0]"

redact_routes and route_templates follow the same rule, see Route redaction.

takt() Twig function

Render the snippet in your base template’s <head>:

<head>
    <meta charset="utf-8">
    {{ takt() }}
</head>

takt() takes no arguments: autocapture (outbound, files, tagged, not_found) and every other option are set in config/packages/takt.yaml only. An argument passed from a template is ignored without error. There is no per-template override.

Autowired Takt service (server-to-server)

Just type-hint Vskstudio\Takt\Takt, autowiring does the rest:

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Vskstudio\Takt\Revenue;
use Vskstudio\Takt\Takt;

final class CheckoutController
{
    public function __construct(private readonly Takt $takt) {}

    public function success(): Response
    {
        $this->takt->event('Purchase', ['plan' => 'pro'], new Revenue(amount: '29', currency: 'EUR'));

        return new Response('Thanks');
    }
}

Sending is fire-and-forget: a 202 counts as success, and transport errors are swallowed so analytics never breaks the request. $this->takt->strict() returns an instance that throws on any status other than 202 (useful in tests). pageview() and withVisitor() are covered on the PHP core page.

The service derives the visitor IP through Request::getClientIp(), which only honors X-Forwarded-For when framework.trusted_proxies is configured. Behind an undeclared CDN or reverse proxy, every S2S event is attributed to the proxy's IP.
In mode: asset the loader points at /takt/takt.auto.js, but the bundle publishes no asset: copy vendor/vskstudio/takt-core-php/resources/takt.auto.js to public/takt/takt.auto.js yourself. In mode: sdk with script_origin, the module is loaded from /takt/takt.esm.js on that origin. That file is not vendored either, so you must serve it.

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 (PHP core 0.6), two configuration nodes replace real paths with the route pattern, in the takt() snippet and in the autowired Takt service alike (see Route redaction). In the snippet they require mode: sdk: in any other mode the SnippetRenderer throws, since the minimal snippet cannot redact.

redact_routes

takt:
    mode: sdk
    redact_routes: ['/verify/{token}', '/reset/{code}']

A path matching one of the patterns is sent as the pattern, every other path keeps its real value. Patterns accept the Symfony syntax ({token}, {page?}) as well as [p], [[p]], [...p], (group), :p, :p?, * and ** (details on the PHP core page). Inline requirements and defaults are stripped from the pattern that is sent ({id<\d+>?1} becomes {id}).

The browser SDK does not read braces: {token} is handed to it as [token]. A browser pageview therefore reports /verify/[token], while an event sent by the Takt service reports /verify/{token}. Write /verify/[token] if both must land on the same row.

route_templates

takt:
    mode: sdk
    route_templates: true

Every page is sent as its route path: /users/42 becomes /users/{id}. The bundle then registers a private Vskstudio\Takt\Symfony\RouteTemplate service, which resolves the _route of the main request through the router (Route::getPath()). That path is rendered into the takt() snippet and used as the default route of every pageview() and event() of the autowired Takt service. It is not translated to brackets: browser and server both report /users/{id}. A request with no matched route keeps its real path, still subject to redact_routes.

route_templates suits a private app; on a public site it merges all your articles into a single row of the Pages report.

Resolving a route path loads the router's route collection. Under PHP-FPM, where nothing persists between requests, this mode therefore adds a small cost to every request that renders the snippet or sends an S2S event.

redact_undecodable_paths

takt:
    redact_undecodable_paths: false

A boolean, true by default, since bundle 0.7.0: an unreadable path (a malformed % escape or invalid UTF-8 once decoded) is sent as /[redacted] as soon as a route is configured. With false, it is matched against the routes like any other (see Unreadable paths). The node only sets the autowired Takt service (server events); the takt() snippet keeps the default, masked.

Explicit template

Any server-side call can pick its own template, which wins over the default:

$this->takt->event('Verified', url: $request->getUri(), route: '/verify/{token}');

$this->takt->withRoute('/invoices/{id}')->pageview($request->getUri());

withRoute() returns an instance whose calls are all sent under that template; it also accepts a closure, resolved at send time. Argument details on the PHP core page.