onetracepro / onetrace-laravel
Laravel integration for the OneTrace.pro customer data platform: events after the response or via queues, tracker Blade directive, automatic identify, Eloquent product sync and a test fake.
Requires
- php: ^8.1
- illuminate/auth: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/bus: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/console: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/container: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/cookie: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/database: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/events: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/mail: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/queue: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/view: ^10.0 || ^11.0 || ^12.0 || ^13.0
- onetracepro/onetrace-php: ^1.16
Requires (Dev)
- orchestra/testbench: ^8.0 || ^9.0 || ^10.0 || ^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.5.50 || ^12.5.8 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel integration for OneTrace.pro, the customer data platform. Track orders and sign-ups from your backend without slowing responses down, put the website tracker on your pages with one Blade directive, keep the product catalog in sync with your Eloquent models, and test it all with a fake.
Built on onetracepro/onetrace-php: everything that library can do is available through the facade.
- Laravel 10–13, PHP 8.1–8.5
- Events go out in one batch after the response, through your queue, or immediately
anonymousIdfrom the tracker cookie and the signed-in user are added to events automaticallyidentifyon login and registration, tracker reset after logoutOneTrace::fake()for your application's tests
Installation
composer require onetracepro/onetrace-laravel
Add the keys from your project's API keys section to .env:
ONETRACE_URL=https://cdp.onetrace.pro # or the domain of your white-label brand ONETRACE_WRITE_KEY=cdp_wk_… # events and the website tracker ONETRACE_SECRET_KEY=cdp_sk_… # products, profiles, segments, journeys, campaigns
To change the defaults, publish the config: php artisan vendor:publish --tag=onetrace-config. php artisan about shows the current settings.
Website tracker
Put the directive into the <head> of your layout:
<head> … @onetrace </head>
It prints the tracker install code with your write key and records the page view. For a signed-in user it links the browser to the user's id (only the id is printed into the page, no personal data), and on the first page after logout it resets the visitor so the next person on the device starts fresh. Options: @onetrace(['nonce' => $nonce, 'page' => false, 'ignore_bots' => false, 'language' => 'page']); Vite's CSP nonce is used automatically. language — the visitor's language for translated products and template versions: page (the lang of <html>, e.g. app()->getLocale() in your layout), auto (the browser, by default), a code or false.
Search engine crawlers, link previews, monitoring services and automated browsers (Playwright, Selenium) are not tracked: the tracker sends no events and sets no cookies for them, while recommendation widgets are still shown. To track everyone — for example, to test the integration with an automated browser — set ONETRACE_IGNORE_BOTS=false (or 'ignore_bots' => false in the directive) in that environment. Product events, widgets and Web Push in the browser are described in your account under Site.
Product search
The plan of your project must include product search. A search box with suggestions while typing (products, categories, popular queries):
<x-onetrace::search-box action="/search" category-url="/catalog/{id}" placeholder="Search" class="w-full" />
Your results page:
$result = OneTrace::searchProducts($request->query('q', ''), [ 'page' => $request->integer('page', 1), 'per_page' => 24, 'sort' => 'relevance', 'category' => $request->query('category'), ]); // $result['items'], $result['total'], $result['facets'] (categories, brands, price, attributes) // $result['corrected'] / $result['suggestion'] — typo fixed or "did you mean"; $result['filters'] — price, brand and // characteristics recognized in the text; $result['unavailable'] — over the monthly search limit: show your own search
Or let the tracker draw the whole results page — turn on the search widget in Site → Search and put the container on your page; the "Add to cart" button posts the quantity to your address and expects {"ok": true} (or {"redirect": "…"} for a product that needs options):
<x-onetrace::search-results cart-url="/cart/add/{id}" />
The visitor of the tracker cookie (the order by their interests) and app()->getLocale() (names in the catalog translation) are added automatically; the first page records the search event, from which the popular queries of the suggestions come. Suggestions from the backend: OneTrace::search()->suggest('lin').
Events from the backend
use OneTrace\Laravel\Facades\OneTrace; OneTrace::track('order_completed', [ 'order_id' => $order->number, 'amount' => $order->total, 'products' => $order->lines->map(fn ($line) => [ 'product_id' => $line->sku, 'quantity' => $line->quantity, 'price' => $line->price, ])->all(), ], ['messageId' => 'order-' . $order->number]); // a repeat within 24 hours is ignored OneTrace::identify($user, ['plan' => 'pro']); // the user's email and name are added OneTrace::identify(null, ['email' => $request->email]); // a visitor who left an email in a form OneTrace::page('Checkout'); OneTrace::alias($oldUserId); // merge an old id into the signed-in user
Each event gets the signed-in user (userId), the visitor id (anonymousId, see below), the IP and user agent, a messageId and a timestamp. Calls validate the event right away and return immediately; sending depends on ONETRACE_SEND:
ONETRACE_SEND |
|
|---|---|
after_response (default) |
one batch after the response is sent to the browser; after each job in queue workers; at the end of artisan commands |
queue |
the same batch is pushed as a job (ONETRACE_QUEUE_CONNECTION, ONETRACE_QUEUE); retries keep the same messageIds |
sync |
sent immediately, the call waits for the API |
Errors of background sending are reported to your exception handler; they never break the response. OneTrace::flush() sends right away (useful in long-running loops). ONETRACE_ENABLED=false turns sending off; without keys nothing is sent either.
Visitor id before sign-in
Events before sign-in belong to an anonymous visitor; when the same anonymousId comes with the user's identify, the visitor's history is merged into the user's profile. On websites with the tracker this works by itself — the id comes from the tracker's cookie. Elsewhere, tell the package where the id is:
// For the rest of the request or job — including the automatic identify on login and registration: OneTrace::useAnonymousId($request->input('anonymous_id')); // Mobile apps and SPAs send it in a header — in .env: ONETRACE_ANONYMOUS_HEADER=X-Anonymous-Id // Any other source, once in a service provider: OneTrace::resolveAnonymousIdUsing(fn (?Request $request) => $request?->session()->get('visitor_id')); // One call only: OneTrace::identify($user, [], ['anonymousId' => $visitorId]);
The first one found wins: useAnonymousId(), the resolver, the header, the tracker cookie. Registration handled in a queued job: pass the id with the job and call OneTrace::useAnonymousId() in it. An id the user had on another device can be merged later with OneTrace::alias($oldId).
Automatic identify
On Laravel's Login and Registered events the user is identified with email and name. To choose the traits, implement HasOneTraceTraits on the user model:
use OneTrace\Laravel\Contracts\HasOneTraceTraits; class User extends Authenticatable implements HasOneTraceTraits { public function oneTraceTraits(): array { return ['email' => $this->email, 'phone' => $this->phone, 'first_name' => $this->first_name, 'plan' => $this->plan]; } }
email, phone, telegram_chat_id and viber_id identify the profile; the rest become profile attributes. Turn it off with onetrace.identify.on_login / on_register.
Product catalog
use OneTrace\Laravel\Contracts\OneTraceProduct; use OneTrace\Laravel\Eloquent\SyncsWithOneTrace; class Product extends Model implements OneTraceProduct { use SyncsWithOneTrace; public function toOneTraceProduct(): array { return [ 'id' => $this->sku, // the product_id your events use 'name' => $this->title, 'price' => $this->price, 'currency' => 'EUR', 'url' => route('products.show', $this), 'image' => $this->image_url, 'category_ids' => [$this->category_slug], 'available' => $this->stock > 0, 'description' => $this->description, // HTML or text: the AI answers shoppers' questions from it ]; } public function shouldSyncWithOneTrace(): bool { return $this->is_published; // optional } }
Saved models are created or updated in the catalog, deleted ones removed (soft-deleted too; restored ones come back) — sent the same way as events, in batches of up to 1000. Upload the whole catalog once, or on a schedule:
php artisan onetrace:sync-products "App\Models\Product"
Without models: OneTrace::syncProducts([['id' => 'SKU-1', 'name' => '…']]) and OneTrace::deleteProducts(['SKU-1']). Products need the secret key.
Other languages of your store go with the product: 'translations' => ['en' => ['name' => 'Sneakers', 'url' => route('products.show', [$this, 'locale' => 'en']), 'description' => $this->description_en]] in toOneTraceProduct(). Recommendations, widgets, search and emails show the translation in the visitor's or the message's language.
The description (and its translations) is sent as text: HTML tags and entities are removed, paragraphs and list items become lines, at most 5000 characters; an empty one is left out.
Ask AI about the product
Shoppers ask questions on the product page and the AI answers from the product description and characteristics of the catalog and the knowledge base of your store, in the language of the question. Turn the widget on in Site → Product questions and put the element on the product page:
<x-onetrace::ask :product="$product->sku" />
product is the id of the catalog (product_id of events). The tracker of @onetrace draws suggested questions and the answer, or a chat button with the product, as set up in OneTrace; while the widget is off (or AI is not paid for) the element stays empty.
Emails through OneTrace
The mail transport onetrace sends the application's emails through the project's email integration of OneTrace.pro as is — with delivery statuses and history in the customer profile. Put it in a failover mailer: when OneTrace does not accept an email (no email integration, the message quota is used up) or is unavailable, the next mailer sends it. The secret key needs the messages.send permission.
// config/mail.php 'default' => env('MAIL_MAILER', 'failover'), 'mailers' => [ 'onetrace' => ['transport' => 'onetrace', 'kind' => 'other'], 'failover' => ['transport' => 'failover', 'mailers' => ['onetrace', 'smtp']], // smtp, log … ],
Mark the kind of an email for reports and the profile history:
use OneTrace\Laravel\Mail\SendsThroughOneTrace; class ResetPassword extends Mailable { use SendsThroughOneTrace; public function build(): static { return $this->subject('Reset your password')->view('mail.reset')->oneTraceKind('password_reset'); } }
- Every recipient (To, Cc, Bcc) gets their own copy; attachments are PDF, images, text, CSV or calendar files, up to 5 MB in total.
- Access-link kinds (
password_reset,account_new,email_verification,login_link,login_code) go without click tracking and keep only the subject after sending. - Order emails can be built from OneTrace templates instead: send
OneTrace::emails()->send()withorderdata (see the library's README, "Store emails").
Store MCP server for the AI chat bot
The AI chat bot of OneTrace can answer your customers with live data of your store — order status, tracking, stock, delivery, returns — and, if you allow it, cancel orders, create returns and change delivery. It calls an MCP server of your application by the store MCP contract. Implement the tools you offer:
namespace App\OneTrace; use App\Models\Order; use App\Models\User; use OneTrace\Laravel\StoreMcp\StoreMcpHandlers; use OneTrace\StoreMcp\{Context, ToolError}; final class StoreTools extends StoreMcpHandlers { public function getCustomer(Context $ctx): array { $user = User::findOrFail($ctx->customerId); return ['id' => (string) $user->id, 'email' => $user->email, 'first_name' => $user->first_name]; } public function getOrder(array $args, Context $ctx): array { $order = Order::where('user_id', $ctx->customerId)->where('number', $args['order'])->first() ?? throw new ToolError(ToolError::NOT_FOUND, __('No such order.')); return ['order' => $order->number, 'status' => $order->status, 'total' => ['amount' => $order->total, 'currency' => 'EUR']]; } // Confirmation by a code sent by email (override sendCode() for SMS). public function findCustomer(string $contact, ?string $order): ?string { return User::where('email', $contact)->value('id'); } }
ONETRACE_STORE_MCP=true ONETRACE_STORE_MCP_KEY= # php artisan onetrace:store-mcp --key
Set 'store_mcp.handlers' => App\OneTrace\StoreTools::class in config/onetrace.php and run php artisan onetrace:store-mcp once (secret key with chats.manage): the platform checks the server at POST /onetrace/mcp and the bot starts using it.
- Method names follow the tools:
getCustomer,findOrders,getOrder,getTracking,checkAvailability,getDeliveryOptions,getReturnPolicy,cancelOrder,createReturn,changeDelivery; a tool without a method is not offered.$ctx->customerIdis the confirmed customer,$ctx->language— the conversation language. - Read tools are offered by default; list actions in
store_mcp.toolsto allow them. Actions run only after the customer agreed in the chat, with$ctx->idempotencyKey. @onetraceconfirms a signed-in user for the bot: it passes a signed customer token to the tracker (cdp.chatCustomer()). Guests and messenger customers confirm with a code (findCustomer()+sendCode()).
The rest of the API
The facade passes everything else to the OneTrace\Client:
use OneTrace\Identity; OneTrace::profiles()->get('email', 'anna@example.com'); OneTrace::profiles()->updateConsent('user_id', '42', 'email', 'unsubscribed'); OneTrace::profiles()->preferences('user_id', $user->id, app()->getLocale()); // topics by channel, frequency, pause, preferences_url OneTrace::profiles()->updatePreferences('user_id', $user->id, ['frequency' => 'weekly', 'topics' => ['news' => ['email' => false]]]); OneTrace::segments()->addMembers(12, [Identity::userId($user->id)]); OneTrace::journeys()->enroll(7, Identity::userId($user->id), ['order_id' => 'A-1001'], 'order-A-1001'); OneTrace::recommendations()->get('viewed_with', ['item' => $product->sku, 'limit' => 4]); OneTrace::analytics()->funnel(['steps' => [['event' => 'product_viewed'], ['event' => 'order_completed']], 'period' => ['last' => 30]]); OneTrace::loyalty()->calculate(Identity::email($user->email), $cart->total()); OneTrace::loyalty()->refer(Identity::userId($user->id), OneTrace::referralCode()); // on registration: the code of a ?ref= link (cookie cdp_ref) OneTrace::promo()->issue('welcome', Identity::userId($user->id)); // a promo code of a pool OneTrace::messages()->send(Identity::email($user->email), 12, ['data' => ['order_id' => $order->number]], "order-{$order->number}"); // a transactional message OneTrace::chats()->reply($conversationId, 'Your order ships today.'); // answer a chat conversation as the API key OneTrace::emails()->send(['to' => $user->email, 'kind' => 'order_shipped', 'order' => $order->emailData([...])]); // an order email from the OneTrace template OneTrace::client(); // the client itself, also injectable as OneTrace\Client
A «Subscriptions» page in your customer account can show preferences() as a form and save it with updatePreferences(), or link to the hosted preference center: preferences_url is signed and valid for 30 days.
See the library's README for errors, retries and pagination, and the API reference.
Testing your application
use OneTrace\Laravel\Facades\OneTrace; public function test_checkout_tracks_the_order(): void { $fake = OneTrace::fake(); $this->actingAs($user)->post('/checkout', $payload); $fake->assertTracked('order_completed', fn (array $message) => $message['properties']['amount'] === 9980); $fake->assertIdentified($user->id); $fake->assertProductSynced('SKU-1'); $fake->assertNotTracked('refund_issued'); }
Also: assertTrackedTimes(), assertPageViewed(), assertProductDeleted(), assertNothingSent(), sent(). Calls through the client (OneTrace::segments()->…) are recorded too: $fake->respondWith([...]) sets the response, $fake->requests() returns what was sent.
Configuration
| Key | Env | Default | |
|---|---|---|---|
url |
ONETRACE_URL |
https://cdp.onetrace.pro |
your account's address |
write_key |
ONETRACE_WRITE_KEY |
— | events, tracker |
secret_key |
ONETRACE_SECRET_KEY |
— | products and the management API |
enabled |
ONETRACE_ENABLED |
true |
|
send |
ONETRACE_SEND |
after_response |
after_response, queue, sync |
queue.connection, queue.name |
ONETRACE_QUEUE_CONNECTION, ONETRACE_QUEUE |
default | for queue |
language |
ONETRACE_LANGUAGE |
en |
language of API error messages |
timeout, max_retries |
ONETRACE_TIMEOUT, ONETRACE_MAX_RETRIES |
10, 3 |
|
anonymous_cookie |
cdp_aid |
the tracker's visitor cookie | |
anonymous_header |
ONETRACE_ANONYMOUS_HEADER |
— | request header with the visitor id (mobile apps, SPAs) |
identify.on_login, identify.on_register |
true |
automatic identify | |
tracker.identify, tracker.reset_on_logout |
true |
@onetrace behaviour |
|
tracker.ignore_bots |
ONETRACE_IGNORE_BOTS |
true |
skip crawlers and automated browsers in the tracker |
tracker.language |
ONETRACE_TRACKER_LANGUAGE |
— | the visitor's language in @onetrace: page, auto, a code |
Octane and queue workers
The package works under Laravel Octane (checked on Swoole) and in long-running queue workers: the collector is scoped to the request or job, events are sent when each request terminates or each job finishes, and useAnonymousId() never leaks into the next request.
The tracker's cookies (cdp_aid, cdp_ref) are set by JavaScript and are not encrypted, so the package excludes them from EncryptCookies (Laravel 11+). On Laravel 10 add them yourself — required under Octane, recommended everywhere:
// app/Http/Middleware/EncryptCookies.php protected $except = ['cdp_aid', 'cdp_ref'];
Development
composer install
composer check # PHPStan and PHPUnit (Testbench)
License
MIT, see LICENSE.