WordPress analytics in one prompt
undefined
Paste the prompt below into a coding agent that can reach your WordPress code (your theme or plugin repository, or the server over SSH). It gets an AgentFirstStack key, creates the site, serves the tracker from your own domain, adds it to every page with wp_head, and checks the live site until the setup check passes. Form submissions and WooCommerce orders can be sent from PHP, where visitors can't fake them. No access to the code at all? The plugin route at the end needs only wp-admin. Other platforms and tools: add analytics with an AI agent.
The prompt
Add analytics to this WordPress site with AgentFirstStack. It's free, open-source analytics built for agents, and you can do all of it yourself: there is no signup form, and no email or verification is needed. 1. Read https://agentfirststack.com/llms.txt and https://analytics.agentfirststack.com/llms.txt. 2. If wp-config.php or .env already has AGENTFIRSTSTACK_API_KEY, use it and read AGENTFIRSTSTACK.md. Otherwise get a key with one request: curl -X POST https://agentfirststack.com/v1/signup Store it as a constant in wp-config.php (define('AGENTFIRSTSTACK_API_KEY', '...')), never in theme files or the repository. Name the project after the domain: PATCH https://agentfirststack.com/v1/me. 3. Create a site for the production domain and read GET /v1/sites/:id/install?framework=wordpress. Serve the tracker from the site's own domain: a Cloudflare Worker route on /_a/* if the domain is on Cloudflare, else the nginx block if we run nginx, else a must-use plugin that forwards /_a/* in PHP. The proxy key (first_party_proxy.proxy_key) goes in wp-config.php as AFS_PROXY_KEY or in the Worker's secrets. Then print <script defer src="/_a/s.js" data-site="..."> from a wp_head hook in a must-use plugin (wp-content/mu-plugins), not in a parent theme. 4. Create goals for sign-ups, purchases, form submissions and the main buttons. Send successful form submissions and paid WooCommerce orders from PHP as server events. 5. Add an email destination, a weekly report, and alerts for a traffic drop and for no data. 6. Deploy, purge any page cache, open the site once, then run verify and fix what it reports. 7. Write the site id and where the keys live (never the values) in AGENTFIRSTSTACK.md, and create a claim link for me with POST https://agentfirststack.com/v1/claim-link. 8. Run GET https://agentfirststack.com/v1/setup?services=analytics and work through its next_steps until it says "ready": true. When you're done, tell me what you set up, how you verified it, and give me the claim link.
Where the tag goes
wp_head "prints scripts or data in the head tag on the front end", and every well-behaved theme calls it, so a hook on it covers every page. GET /v1/sites/:id/install?framework=wordpress suggests a child theme's functions.php. A must-use plugin is sturdier: it loads before normal plugins, is enabled on every site of the install, can't be switched off in wp-admin, and survives a theme change. Code in a parent theme's functions.php is lost on the next theme update.
<?php
// wp-content/mu-plugins/agentfirststack.php
add_action( 'wp_head', function () {
echo '<script defer src="/_a/s.js" data-site="site_..."></script>';
} );
The site id is public. The two secrets, the API key and the proxy key, go in wp-config.php, which is outside the theme and normally outside version control.
Serve it from your own domain
Ad blockers block requests to analytics.agentfirststack.com. From your own domain, the script and its collect endpoint (/_a/e, next to wherever the script was loaded from) are first-party, so those visitors count too. Every proxy must add x-afs-proxy-key, the site's secret from first_party_proxy.proxy_key. The collector trusts the visitor IP and location headers only from requests that carry it; without it every visitor arrives with your server's address and merges into one, and verify reports proxy_key. Pick the first option that fits your hosting:
1. Your domain is on Cloudflare (the orange cloud is on). A Worker on the route example.com/_a/* runs in front of WordPress and sends the visitor's exact country, region and city. Store the key as a Worker secret (wrangler secret put AFS_PROXY_KEY):
// Cloudflare Worker, route: example.com/_a/*
export default {
async fetch(request, env) {
const url = new URL(request.url);
const headers = new Headers(request.headers);
for (const k of [...headers.keys()]) if (k.startsWith("x-afs-")) headers.delete(k); // never pass on visitor-sent values
headers.set("x-afs-proxy-key", env.AFS_PROXY_KEY);
headers.set("x-forwarded-for", request.headers.get("cf-connecting-ip") || "");
const cf = request.cf || {};
const geo = { country: cf.country, region: cf.region, "region-code": cf.regionCode, city: cf.city,
continent: cf.continent, timezone: cf.timezone, lat: cf.latitude, lon: cf.longitude };
for (const [k, v] of Object.entries(geo)) if (v != null) headers.set("x-afs-geo-" + k, encodeURIComponent(String(v)));
headers.delete("cookie");
return fetch("https://analytics.agentfirststack.com" + url.pathname + url.search, {
method: request.method,
headers,
body: request.method === "POST" ? request.body : undefined,
});
},
};
2. You run the server's nginx. The install answer's first_party_proxy.nginx is a location /_a/ block, filled in with the key, that passes the visitor IP (and, behind Cloudflare, the country). Without Cloudflare in front, nginx can't know the visitor's location, so country and city show your server's.
3. Managed hosting, no server config. A must-use plugin can answer /_a/* itself. With pretty permalinks on, as on most sites, WordPress sends requests for paths that aren't files to index.php, so the plugin sees them before any page is rendered:
<?php
// wp-content/mu-plugins/agentfirststack-proxy.php
// Needs define( 'AFS_PROXY_KEY', '...' ) in wp-config.php.
add_action( 'plugins_loaded', function () {
$path = (string) parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
if ( strpos( $path, '/_a/' ) !== 0 || ! defined( 'AFS_PROXY_KEY' ) ) {
return;
}
if ( ! defined( 'DONOTCACHEPAGE' ) ) {
define( 'DONOTCACHEPAGE', true ); // keep page caches from storing the answer
}
$post = ( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) === 'POST';
$query = isset( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] !== '' ? '?' . $_SERVER['QUERY_STRING'] : '';
$res = wp_remote_request( 'https://analytics.agentfirststack.com' . $path . $query, array(
'method' => $post ? 'POST' : 'GET',
'timeout' => 5,
'user-agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'headers' => array(
'x-afs-proxy-key' => AFS_PROXY_KEY,
'x-forwarded-for' => $_SERVER['REMOTE_ADDR'] ?? '',
'accept-language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
'content-type' => 'text/plain',
),
'body' => $post ? file_get_contents( 'php://input' ) : null,
) );
if ( is_wp_error( $res ) ) {
status_header( 502 );
exit;
}
status_header( (int) wp_remote_retrieve_response_code( $res ) );
foreach ( array( 'content-type', 'cache-control' ) as $h ) {
$v = wp_remote_retrieve_header( $res, $h );
if ( $v ) {
header( "$h: $v" );
}
}
echo wp_remote_retrieve_body( $res );
exit;
}, 0 );
Two limits to accept with this one. Your PHP server can't see where visitors are, so country and city show your server's location (the visitor counts are right; the IP is passed on). And if your host puts a load balancer in front, REMOTE_ADDR may be the balancer's address; ask the host which header carries the visitor's IP. If location matters more to you than visitors with ad blockers, use the direct snippet instead: src="https://analytics.agentfirststack.com/s.js".
Forms and orders from PHP
For a quick read on forms, add data-forms="true" to the script tag: it sends form_start and form_submit with the form's id or name, never the field values. A submit isn't a success, though. To count only submissions that went through, send a server event from the form plugin's hook. These hooks run in the visitor's own request, so passing their IP address and user agent joins the event to their visit, exactly as the browser would. Behind Cloudflare, use $_SERVER['HTTP_CF_CONNECTING_IP'] instead of REMOTE_ADDR:
function afs_event( $name, array $extra = array() ) {
wp_remote_post( 'https://analytics.agentfirststack.com/v1/sites/site_.../events', array(
'blocking' => false,
'headers' => array(
'Authorization' => 'Bearer ' . AGENTFIRSTSTACK_API_KEY,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array( 'events' => array( array_merge( array(
'name' => $name,
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
), $extra ) ) ) ),
) );
}
// Contact Form 7: fires after the mail was sent
add_action( 'wpcf7_mail_sent', function ( $form ) {
afs_event( 'contact_submit', array( 'props' => array( 'form' => $form->title() ) ) );
} );
// WPForms: only for entries saved without errors
add_action( 'wpforms_process_complete', function ( $fields, $entry, $form_data ) {
afs_event( 'contact_submit', array( 'props' => array( 'form' => $form_data['settings']['form_title'] ?? '' ) ) );
}, 10, 3 );
// Gravity Forms: note it also runs for entries marked as spam
add_action( 'gform_after_submission', function ( $entry, $form ) {
afs_event( 'contact_submit', array( 'props' => array( 'form' => $form['title'] ) ) );
}, 10, 2 );
WooCommerce. A payment often completes in the payment provider's callback, not the shopper's request, so use the IP address and user agent WooCommerce saved on the order. Hook both statuses a paid order can reach (WooCommerce sets Processing or Completed when a payment completes; offline payments such as cheques wait On-Hold until you move them on), and mark the order so it's sent once. visit: false keeps a late callback from creating a visit of its own; if the shopper's visit is still live, the purchase joins it:
function afs_purchase( $order_id, $order ) {
if ( $order->get_meta( '_afs_sent' ) ) {
return;
}
$order->update_meta_data( '_afs_sent', 1 );
$order->save();
afs_event( 'purchase', array(
'ip' => $order->get_customer_ip_address(),
'user_agent' => $order->get_customer_user_agent(),
'visit' => false,
'revenue' => array( 'amount' => (float) $order->get_total(), 'currency' => $order->get_currency() ),
'props' => array( 'order_id' => (string) $order->get_order_number() ),
) );
}
add_action( 'woocommerce_order_status_processing', 'afs_purchase', 10, 2 );
add_action( 'woocommerce_order_status_completed', 'afs_purchase', 10, 2 );
Then create the goals: {"name":"Contact","type":"event","match":"contact_submit"} and {"name":"Purchase","type":"event","match":"purchase"}.
Check it on the live site
- Purge the page cache (your caching plugin's or your host's). Until you do, cached pages are served without the tag.
- Open the site once in a normal browser, logged out. Headless browsers are filtered out unless the visit is marked with
?afsa_debug=1. - Run verify:
POST https://analytics.agentfirststack.com/v1/sites/:id/verify. It finds the tag in your HTML, fetches/_a/s.jsthrough your proxy, checks the Content-Security-Policy and whether events arrived with the proxy key, and lists fixes. - Run the setup check:
GET https://agentfirststack.com/v1/setup?services=analyticsuntil"ready": true.
Script optimizers. WP Rocket's "Delay JavaScript execution" holds back scripts "until there is a user interaction", so visitors who leave without touching the page are never counted. Add /_a/s.js under Excluded JavaScript Files (File Optimization tab), or add the nowprocket attribute to the tag. Other optimizers with a "delay JS" option need the same exclusion.
No code access: the wp-admin route
On a site where you only have wp-admin, paste the direct snippet into a header plugin such as WPCode (Insert Headers and Footers), under its Code Snippets menu:
<script defer src="https://analytics.agentfirststack.com/s.js" data-site="site_..."></script>
That route can't proxy, so visitors with ad blockers aren't counted. On WordPress.com, installing plugins is available on every paid plan (Personal, Premium, Business and Commerce), according to WordPress.com's support pages. You can still run the API side (creating the site, goals, verify, the setup check) from any agent with a terminal.
FAQ
Theme functions.php or a plugin?
A must-use plugin in wp-content/mu-plugins. It survives theme updates and theme switches and can't be deactivated by accident. Code in a parent theme's functions.php disappears on the theme's next update.
Where should the API key live?
In wp-config.php as a constant, read only by PHP. Never in a theme file, a page, the database options table or a <script> tag. The tracker needs only the public site id.
Why do all my visitors show the same country?
You're behind a proxy that can't see visitor locations: the PHP plugin, or nginx without Cloudflare in front. Visitor counts are still right. For exact locations, put the domain on Cloudflare and use the Worker route.
Does it track logged-in admins?
Yes, like any visitor. Exclude yourself by running localStorage.afsa_ignore = 1 in your browser's console on the site, or exclude your office IP in the site's exclusions.
Does the tracker use cookies?
No. By default a visitor is a hash of a daily salt, the site, the IP address and the user agent. Nothing is stored in the browser unless you switch the site to persistent mode.
Related
- AnalyticsAdd analytics with an AI agentThe prompt, the checks, and every tool and platform.
- AnalyticsWebflow analytics in one promptHead code, publishing and form webhooks.
- AnalyticsShopify analytics in one prompttheme.liquid, custom pixels and order webhooks.
- AnalyticsAdd analytics with Claude CodeOne prompt in the terminal, then questions over MCP.
- Server-side conversion trackingMeta, Google Ads, TikTok and GA4, counted once.
- All servicesOne MCP server for every serviceAnalytics, tags, mail and replay tools on one key.
- AnalyticsGoogle Analytics MCP comparedRead GA4, or let the agent create the analytics.
- ReplaySession replay for AI agentsFind out why visitors drop off.