If your website runs on PHP, for example WordPress or a custom PHP site, you can set up the proxy for server-side cookies with a single PHP file. The script forwards everything under https://yourdomain.com/tb/ to Triggerbee. It only passes along Triggerbee's own cookies and the headers Triggerbee needs, so your site's other cookies are never sent to us.
This guide covers Step 1 of First-party and server-side cookies with Triggerbee. Read that article first.
Before you start
PHP 8 or later with the curl extension. Most hosting providers have this enabled. You can check with
php -m | grep curl.Access to your web server configuration or your site's files. You'll most likely need a developer for this step.
Step 1: Add the script to your site
Save the script below as a file on your web server, for example triggerbee-proxy.php in your site's root folder.
Two settings at the top can be changed. Nothing else in the file needs changes.
Setting | What it does | Default |
| The path the proxy is served from. |
|
| The number of proxies (load balancer, CDN) in front of your PHP server. See Set the number of proxies below. |
|
php
<?php
// Serves Triggerbee from your own domain, forwarding only Triggerbee's own cookies and the headers it reads
// Set $prefix to where you want it mounted, and $trustedProxyHops to the number of proxies (load balancer, CDN) in front of PHP.
// Nothing else in this file needs changes. Needs PHP 8 with the curl extension.
(static function (): void {
$prefix = '/tb';
$trustedProxyHops = 0;
$upstream = 'https://t.myvisitors.se';
$eventPaths = '#^/((3/|4/)?t)$#';
$forwardedHeaders = [
'accept', 'accept-language', 'content-type', 'origin', 'referer', 'user-agent',
'access-control-request-headers', 'access-control-request-method', 'if-modified-since', 'if-none-match', 'x-application-id',
];
$forwardedHeaderPrefix = 'x-triggerbee-';
$forwardedCookies = ['_smtruid', '_mtruid'];
$forwardedCookiePrefix = 'triggerbee';
// curl negotiates compression with Triggerbee and decodes the body, so the web server compresses it for the browser as usual.
$droppedResponseHeaders = [
'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade',
'content-encoding', 'content-length', 'nel', 'report-to',
];
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$queryStart = strpos($uri, '?');
$pathname = $queryStart === false ? $uri : substr($uri, 0, $queryStart);
$query = $queryStart === false ? '' : substr($uri, $queryStart);
$path = str_starts_with($pathname, "$prefix/") ? substr($pathname, strlen($prefix)) : null;
if ($path === null) {
// Run as the rewrite target, any other path is not ours; required from a front controller, the site carries on.
if (get_included_files()[0] === __FILE__) {
http_response_code(404);
exit;
}
return;
}
$headers = ['Expect:'];
foreach (getallheaders() as $name => $value) {
$lowerName = strtolower($name);
if (in_array($lowerName, $forwardedHeaders, true) || str_starts_with($lowerName, $forwardedHeaderPrefix)) {
$headers[] = "$name: $value";
}
}
// The raw header rather than $_COOKIE, which renames cookies with dots or spaces in their names.
$cookies = array_filter(
array_map('trim', explode(';', $_SERVER['HTTP_COOKIE'] ?? '')),
static function (string $pair) use ($forwardedCookies, $forwardedCookiePrefix): bool {
$name = trim(explode('=', $pair, 2)[0]);
return in_array($name, $forwardedCookies, true) || str_starts_with($name, $forwardedCookiePrefix);
}
);
if ($cookies) {
$headers[] = 'Cookie: ' . implode('; ', $cookies);
}
$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
$https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
if ($trustedProxyHops > 0) {
// Counted from our end: entries further left are ones a visitor could have put there.
$chain = array_values(array_filter(array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''))));
$clientIp = $chain[count($chain) - $trustedProxyHops] ?? $clientIp;
$https = $https || strtolower(trim(explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')[0])) === 'https';
}
$headers[] = 'X-Forwarded-Host: ' . ($_SERVER['HTTP_HOST'] ?? '');
$headers[] = 'X-Forwarded-Proto: ' . ($https ? 'https' : 'http');
$headers[] = "X-Client-IP: $clientIp";
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$responseHeaders = [];
// Appended, never resolved as a relative URL, so /tb//other.example/x still goes to Triggerbee.
$curl = curl_init($upstream . $path . $query);
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_ENCODING => '',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_HEADERFUNCTION => static function ($handle, string $line) use (&$responseHeaders): int {
if (str_starts_with($line, 'HTTP/')) {
$responseHeaders = [];
} elseif (str_contains($line, ':')) {
[$name, $value] = explode(':', $line, 2);
$responseHeaders[] = [trim($name), trim($value)];
}
return strlen($line);
},
]);
if ($method === 'HEAD') {
curl_setopt($curl, CURLOPT_NOBODY, true);
}
$requestBody = file_get_contents('php://input');
if ($requestBody !== '' && $requestBody !== false) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $requestBody);
}
$body = curl_exec($curl);
if ($body === false) {
// Tracking must never break the site.
http_response_code(502);
exit;
}
http_response_code(curl_getinfo($curl, CURLINFO_RESPONSE_CODE));
foreach ($responseHeaders as [$name, $value]) {
if (!in_array(strtolower($name), $droppedResponseHeaders, true)) {
header("$name: $value", false);
}
}
// This answer is on your own origin, so a page from Triggerbee must not run as your site; scripts and JSON ignore it.
header('Content-Security-Policy: sandbox');
if (preg_match($eventPaths, $path)) {
header('Cache-Control: no-store');
}
echo $body;
exit;
})();
Step 2: Send /tb/ requests to the script
Choose one of these options. Both work with the same script.
Option | Best when |
A. Web server rule | You can edit your Apache |
B. Include from your site's code | Your site already sends all requests through one PHP file (a front controller), as WordPress and most PHP frameworks do. |
Option A: Web server rule
Apache — add this to your .htaccess file, above any existing rewrite rules (for example the WordPress block):
RewriteEngine On RewriteRule ^tb/ /triggerbee-proxy.php [L]
Nginx — add a location block to your site's server block. Adjust the file path and the PHP-FPM socket to match your server:
location ^~ /tb/ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html/triggerbee-proxy.php;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
If you changed $prefix in the script, use the same path here.
Option B: Include from your site's code
Add this line at the very top of the PHP file that handles all requests, before anything else runs:
require __DIR__ . '/triggerbee-proxy.php';
The script only acts on requests under /tb/. Every other request continues to your site as usual.
WordPress: add the line at the top of wp-config.php, right after <?php. Don't add it to index.php, because WordPress replaces that file when it updates.
Set the number of proxies
The script needs to know the visitor's real IP address so Triggerbee can track them correctly. How it finds it depends on what sits in front of your PHP server:
Your setup |
|
Visitors connect directly to your web server |
|
Only Cloudflare in front of your web server |
|
One load balancer or CDN (not Cloudflare) |
|
Cloudflare and a load balancer |
|
Important: Don't set the number higher than the actual number of proxies. If it's wrong, Triggerbee may record the wrong IP address for your visitors, or the same IP address for everyone.
Exclude /tb/ from page caching
If your site uses page caching, for example a WordPress caching plugin, Varnish or a hosting cache, exclude /tb/ from it. Otherwise one visitor's tracking response can be cached and served to other visitors.
Test the proxy
Open https://yourdomain.com/tb/js?site_id=YOUR_SITE_ID. If you see JavaScript code, the proxy works.
Then continue with Step 2 in First-party and server-side cookies with Triggerbee.
Good to know
Server load: every tracking request now passes through your PHP server. On high-traffic sites, make sure your server has room for the extra requests.
Your site is protected: if Triggerbee can't be reached, only the tracking request fails, with a 502 error. Your pages keep loading as usual.
Only Triggerbee data is sent: the script forwards Triggerbee's own cookies and a short list of headers. Your site's login and session cookies are never sent to Triggerbee.
Troubleshooting
The test URL shows your website's 404 page. Requests to /tb/ aren't reaching the script. Check that your rewrite rule is placed above other rules, or that the require line is at the very top of your front controller.
The test URL shows a blank page with a 404 status. The script is running, but the path doesn't match $prefix. Check that $prefix and your rewrite rule use the same path.
The test URL shows a 502 error. Your server couldn't reach Triggerbee. Check that the curl extension is enabled and that your server is allowed to make outgoing HTTPS requests to t.myvisitors.se.
PHP error: "Call to undefined function curl_init()". The curl extension isn't enabled. Ask your hosting provider to enable it.
All visitors get the same IP address. $trustedProxyHops is set too low for your setup. See Set the number of proxies.
