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
| Key | Default | Role |
|---|---|---|
domain | '' | Site domain (data-domain attribute, and the domain carried by every S2S event) |
endpoint | https://taktlytics.com | Ingest target: the origin or the full collect URL, see below |
script_origin | null | First-party origin serving the runtime and deriving the endpoint from it (dodges ad-blockers); endpoint wins over it |
api_key | null | scope=ingest API key, used by the S2S client only |
mode | inline | Runtime source: inline, cdn, asset or sdk |
outbound | false | Tracks outbound links (outbound token) |
files | false | Tracks downloads (downloads token) |
tagged | false | Tracks elements marked with data-takt-event (tagged token) |
not_found | false | Tracks 404 pages (404 token) |
file_extensions | [] | Restricts which downloads are counted; empty keeps the tracker’s built-in list |
exclude_localhost | true | Drops hits fired from localhost, the most common reason nothing shows up in dev |
nonce | null | CSP 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_templates | false | Sends every page as its route path, requires mode: sdk |
redact_undecodable_paths | true | Unreadable paths matched against the routes instead of /[redacted]; S2S events only |
sample_rate | null | Sends only a fraction (0 to 1) of hits |
track_query | null | Keeps 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_dnt | null | false stops honoring Do-Not-Track |
enabled | null | false = kill-switch, no-op snippet |
scrub_url | null | Raw 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 vendoredtakt.auto.jsbundle 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 value | The snippet POSTs to | The S2S service POSTs to |
|---|---|---|
https://taktlytics.com (default) | https://taktlytics.com/api/event | https://taktlytics.com/api/event |
https://taktlytics.com/api/event | https://taktlytics.com/api/event | https://taktlytics.com/api/event |
/api/event (same-origin proxy) | /api/event on your domain | relative 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.
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.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.
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.