Analytics

Event tracking

Send clicks, add-to-cart, purchases and suggestion clicks from your storefront, tie them to the search with searchId, and a copy-paste script that does it.

How it fits together

  1. Your storefront searches with a public key. The answer carries a searchId.
  2. The storefront keeps that searchId with the results it renders.
  3. When the shopper clicks a result, adds it to the cart or buys it, the storefront posts an event with the searchId of the search that showed the product.
  4. Zaproo ties the event to the search and the reports fill in: click-through, add-to-cart and conversion rates, revenue per query, top products and click positions.

Events are accepted at once and tied to their search a few seconds later, off the request path. Nothing in this flow slows the search box down.

The cabinet has no settings for events: your storefront sends them with the API, and the Analytics page shows the result. Until the first event arrives, that page shows No clicks, carts or purchases yet. You need a public API key whose allowed origins list your shop's domain, and the key of the search profile your storefront searches with.

The endpoint

POST/v1/events/:profileKeyPublic key from an allowed origin, or a secret key

profileKey is the key of the search profile the storefront searched with. The route takes the same keys, the same origin check and the same rate limit as POST /v1/search/:profileKey. The body is a bare JSON array of 1 to 100 events. The answer is 202 { "accepted": n }, where n counts the events in the request (a purchase with three lines is one).

curl -X POST https://api.search.zaproo.cloud/v1/events/$PROFILE_KEY \
  -H "Authorization: Bearer $ZS_PUBLIC_KEY" -H "Origin: https://shop.example.com" -H "Content-Type: application/json" \
  -d '[{"type": "click", "searchId": "8f2Kq9xWv1pLmN3cT6yZ0a", "itemId": "tape-48", "position": 2}]'

Event types

Every event may carry sessionId, the same value the search request sends: 1 to 100 letters, digits, - or _.

click
event
A shopper opened a result. itemId (required), searchId, position (1 and up, the place of the result on the page).
add_to_cart
event
A shopper put a product from the results in the cart. itemId (required), searchId, quantity, price (one unit) and currency (ISO 4217, EUR).
purchase
event
An order was placed. orderId (required), currency (required) and items: 1 to 100 lines of itemId, quantity, price and an optional searchId per line. Send every order, not only the ones that came from search: the share of revenue compares the two.
suggestion_click
event
A shopper picked an autocomplete suggestion. suggestion (the text, required) and itemId when the suggestion was a product.
[
  { "type": "click", "searchId": "8f2Kq9xWv1pLmN3cT6yZ0a", "sessionId": "s_41f", "itemId": "tape-48", "position": 2 },
  { "type": "add_to_cart", "searchId": "8f2Kq9xWv1pLmN3cT6yZ0a", "sessionId": "s_41f", "itemId": "tape-48", "quantity": 1, "price": 1.86, "currency": "EUR" },
  { "type": "purchase", "sessionId": "s_41f", "orderId": "100042", "currency": "EUR",
    "items": [{ "itemId": "tape-48", "quantity": 1, "price": 1.86, "searchId": "8f2Kq9xWv1pLmN3cT6yZ0a" }, { "itemId": "box-300", "quantity": 2, "price": 0.32 }] },
  { "type": "suggestion_click", "sessionId": "s_41f", "suggestion": "duct tape" }
]

searchId and the session fallback

searchId is the 22-character id in every search response. It is null when the search is not logged (an inline profile, the editor's preview), and such a search cannot be tied to events.

An event without a searchId but with a sessionId is tied to the latest live search of that session within the last 24 hours. That covers the cart and the checkout, where the search id is long gone but the session is not. An event with neither is stored unattributed: it counts in the share of revenue, but in no per-query number.

Revenue of a purchase line goes to the search its searchId names, or to the session's last search. Lines from a search get quantity × price added to that query's revenue.

A script for your storefront

With the JavaScript SDK, one call sends any of these events: zaprooSearch.track({ type: 'click', searchId, itemId, position }). Use the script below if you'd rather not add a package.

The snippet below keeps the last searchId per product and sends events with fetch and keepalive: true, which lets the request finish while the page unloads. navigator.sendBeacon cannot send the Authorization header this route needs, so do not use it here.

// zaproo-events.js: searches with a public key and reports what shoppers do next.
const API = 'https://api.search.zaproo.cloud';
const KEY = 'zs_pk_YOUR_PUBLIC_KEY';
const PROFILE = 'YOUR_PROFILE_KEY';
const sessionId = sessionStorage.zsSession ??= Math.random().toString(36).slice(2, 12);
const searchOf = new Map(); // itemId -> searchId of the search that showed it

const headers = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' };

export async function search(body) {
  const res = await fetch(`${API}/v1/search/${PROFILE}`, { method: 'POST', headers, body: JSON.stringify({ ...body, sessionId }) });
  const data = await res.json();
  if (data.searchId) for (const hit of data.items) searchOf.set(hit.id, data.searchId);
  return data;
}

export function track(events) {
  const body = JSON.stringify(events.map((e) => ({ sessionId, ...e })));
  return fetch(`${API}/v1/events/${PROFILE}`, { method: 'POST', headers, body, keepalive: true }).catch(() => {});
}

// Call these from your result list, product page and checkout.
export const click = (itemId, position) => track([{ type: 'click', itemId, position, searchId: searchOf.get(itemId) }]);
export const addToCart = (itemId, quantity, price, currency) => track([{ type: 'add_to_cart', itemId, quantity, price, currency, searchId: searchOf.get(itemId) }]);
export const purchase = (orderId, currency, items) =>
  track([{ type: 'purchase', orderId, currency, items: items.map((i) => ({ ...i, searchId: searchOf.get(i.itemId) })) }]);
export const suggestionClick = (suggestion, itemId) => track([{ type: 'suggestion_click', suggestion, itemId }]);

searchOf lives in memory, so it is empty on the product page after a full navigation. Keep it in sessionStorage if your store is not a single-page app, or rely on the session fallback: the sessionId is enough for the cart and the order to find the last search.

What is stored

Each event is stored with its type, the search it was tied to, the normalized query of that search, the item, position, quantity, price, revenue and currency as sent, the order id, the salted session hash, the device class and country, and the traffic source of the request. Events from an excluded IP address or a crawler are kept but left out of the reports. Events follow the same retention as searches.

Errors

StatusCodeMeaning
400VALIDATION_FAILEDA field is missing or wrong; details names it. The body must be an array of 1 to 100 events.
401UNAUTHENTICATEDNo key, or an unknown, revoked or expired one.
403ORIGIN_NOT_ALLOWEDA public key from an origin that is not on its list.
404PROFILE_NOT_FOUNDNo saved profile with that key in the key's project.
429RATE_LIMITEDThe search rate limit; see Rate limits.

Common mistakes

  • Sending the search id of the wrong search. A product can show up for several searches. Keep the id of the search that showed the result the shopper clicked.
  • Leaving out the session id. Without it, a purchase without searchId per line cannot be tied to anything.
  • Prices per line, not per order. price is one unit; revenue is quantity × price.
  • Tracking from a server with a public key. A server sends no Origin header, so the public key is refused. Use a secret key from a server.