takt

Laravel

The Laravel bridge vskstudio/takt-laravel wires the PHP core into the container: a Blade directive for the snippet, a facade for S2S and a publishable config. 0.5.x — PHP 8.1+, Laravel 10 / 11 / 12.

composer require vskstudio/takt-laravel

The TaktServiceProvider is auto-discovered: SnippetRenderer (snippet rendering) and Takt (S2S client) are bound in the container from config, and the IP / User-Agent are forwarded to the S2S client whenever an HTTP request is available.

This page is the option reference. For the end-to-end integration and its main trap — attributing S2S sends made outside a visitor request, from a queued job, an Artisan command or a webhook — see the guide cookieless analytics in a Laravel app.

Configuration

php artisan vendor:publish --tag=takt-config

takt-config is the package’s only publishable tag. config/takt.php reads environment variables:

TAKT_DOMAIN=example.com
TAKT_API_KEY=tk_…               # required only for S2S
TAKT_MODE=inline                # inline (default) | cdn | asset | sdk
TAKT_ENDPOINT=https://taktlytics.com/api/event   # see "Endpoint and first-party origin"
TAKT_SCRIPT_ORIGIN=https://analytics.example.com # first-party origin serving the tracker
TAKT_EXCLUDE_LOCALHOST=false    # default: true — nothing is measured from localhost
TAKT_NONCE=# CSP nonce for the script tag
TAKT_OUTBOUND=true
TAKT_FILES=true
TAKT_TAGGED=true
TAKT_NOT_FOUND=true
TAKT_FILE_EXTENSIONS=pdf,zip,docx

# Advanced options — leave empty to keep the tracker defaults
TAKT_SAMPLE_RATE=0.5            # send only a fraction (0–1) of hits
TAKT_TRACK_QUERY=true           # keep the query string + hash (default: stripped)
TAKT_QUERY_PARAMS=utm_source,utm_medium  # allowlist when track_query is off
TAKT_EXCLUDE=/app,/account      # path prefixes never tracked — requires TAKT_MODE=sdk
TAKT_RESPECT_DNT=false          # stop honoring Do-Not-Track
TAKT_ENABLED=false              # kill-switch: no-op snippet

TAKT_EXCLUDE_LOCALHOST defaults to true: in local development nothing is measured until you set it to false.

Render modes

TAKT_MODE picks the source of the browser runtime:

TAKT_MODEOutput
inline (default)The vendored takt.auto.js bundle is embedded in an inline <script> tag: no extra request, but inline JS on every page — under a strict CSP, set TAKT_NONCE
cdn<script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/[email protected]/dist/takt.auto.js">
asset<script defer src="/takt/takt.auto.js"> — a self-hosted copy, prefixed with TAKT_SCRIPT_ORIGIN when that origin is set
sdk<script type="module">import{init}…;init({…})</script> — the full SDK, loaded from jsDelivr or from /takt/takt.esm.js on TAKT_SCRIPT_ORIGIN

TAKT_OUTBOUND, TAKT_FILES, TAKT_TAGGED and TAKT_NOT_FOUND each add a token (outbound, downloads, tagged, 404) to the single data-auto attribute the bundle reads, in inline, cdn and asset modes; in sdk mode they are boolean keys (outbound, files, tagged, notFound) of the object passed to init().

The package only publishes its config: no command drops the bundle into public/. For TAKT_MODE=asset, copy vendor/vskstudio/takt-core-php/resources/takt.auto.js to public/takt/takt.auto.js — for instance from a post-update-cmd script in your composer.json, so the copy follows package updates.

Options restricted to sdk mode

TAKT_SCRUB_URL — a raw JS function rewriting URLs, injected verbatim into the page and to be kept dev-controlled: never build it from user input — and TAKT_EXCLUDE only exist in the full SDK. Set in any other mode they are not ignored: building the SnippetRenderer throws an InvalidArgumentException, i.e. a 500 on every page that renders the snippet.

TAKT_MODE=sdk
TAKT_SCRUB_URL="(u) => u.split('#')[0]"
TAKT_EXCLUDE=/app,/account

Endpoint and first-party origin

TAKT_ENDPOINT feeds two consumers that do not read the same thing in the value:

  • the snippet rendered by @takt gets it as-is (data-endpoint attribute, or the endpoint key of the init() call in sdk mode) and POSTs to that exact URL;
  • the Takt facade treats it as a base origin and appends /api/event on every send.
TAKT_ENDPOINTThe snippet POSTs toThe facade POSTs to
https://taktlytics.com (package default)https://taktlytics.com — not the ingest pathhttps://taktlytics.com/api/event
https://taktlytics.com/api/eventhttps://taktlytics.com/api/eventhttps://taktlytics.com/api/event/api/event — not the ingest path

No single value serves both. Pick according to what you use:

  • snippet only (no API key): TAKT_ENDPOINT=https://taktlytics.com/api/event;
  • S2S only: keep the https://taktlytics.com default;
  • both: keep the full URL for the snippet and rebind the S2S client to the origin, in your AppServiceProvider’s register():
use Vskstudio\Takt\Takt;

$this->app->scoped(Takt::class, function ($app) {
    $takt = new Takt('https://taktlytics.com', config('takt.domain'), config('takt.api_key'));
    $request = $app['request'] ?? null;

    return $request === null ? $takt : $takt->withVisitor($request->ip(), $request->userAgent());
});

TAKT_SCRIPT_ORIGIN is the first-party origin you proxy through to Takt to dodge ad-blockers. It is rendered as data-script-origin and serves the tracker file in asset and sdk modes. The tracker only derives the ingest path from it — the origin followed by /api/event — when no endpoint is rendered, which requires TAKT_ENDPOINT=https://taktlytics.com/api/event. With any other value, data-endpoint is rendered and wins over the origin, which then only loads the file.

@takt Blade directive

Drop @takt into your layout’s <head> to render the snippet:

<head>
  <meta charset="utf-8">
  @takt
</head>

The directive takes no argument: it compiles a fixed call to SnippetRenderer::render(), and any expression written between parentheses is discarded without error. The snippet is therefore rendered from config alone.

For a different render — typically a CSP nonce, which is per-request while the SnippetRenderer is a singleton — rebind the renderer before the view is rendered, from a middleware:

use Illuminate\Support\Facades\App;
use Vskstudio\Takt\Options;
use Vskstudio\Takt\SnippetRenderer;

App::bind(SnippetRenderer::class, fn () => new SnippetRenderer(Options::fromArray(
    ['nonce' => $nonce, 'scriptOrigin' => config('takt.script_origin')] + config('takt')
)));

Options::fromArray() reads the other keys in snake_case, like the config; scriptOrigin is the only one expected in camelCase.

Takt facade (server-to-server)

use Vskstudio\Takt\Laravel\Facades\Takt;
use Vskstudio\Takt\Revenue;

Takt::event('Signup', ['plan' => 'pro']);

Takt::event('Purchase', ['plan' => 'pro'], new Revenue(amount: '29', currency: 'EUR'));

Takt::pageview('https://example.com/thanks');
  • event(string $name, array $props = [], ?Revenue $revenue = null, ?string $url = null, ?string $referrer = null): void
  • pageview(?string $url = null, ?string $referrer = null): void
  • Without $url the event is sent with an empty URL: it is counted, but attached to no page.
  • Fire-and-forget: a 202 means success; by default transport errors and non-202 statuses are swallowed, since analytics must never break the app. No exception surfaces — confirm ingestion from the dashboard. Takt::strict() returns an instance that throws on non-202: keep it for tests.

The facade resolves the container’s Takt service, bound per request. The IP and User-Agent come from the current request via $request->ip() and $request->userAgent().

Behind a proxy or a load balancer, configure trusted proxies (App\Http\Middleware\TrustProxies on Laravel 10, trustProxies() in bootstrap/app.php on Laravel 11 / 12): without it Takt receives the proxy's IP and your whole S2S audience is attributed to a handful of infrastructure addresses. Outside an HTTP request — queued job, Artisan command, webhook — there is no visitor to forward: pass the real IP and User-Agent yourself with withVisitor().