If you are using Coveo for Sitecore and seeing InvalidToken or ExpiredToken errors in your Coveo analytics logs, this post covers four issues I ran into and how I fixed them. These are specific to Sitecore XP sites using the Coveo REST proxy for analytics, but the patterns may help anyone dealing with Coveo analytics initialization problems.
The Setup
The site had Coveo analytics initialized in multiple places - a shared JavaScript file (coveo-analytics.js) and several cshtml Razor views (CoveoPageViewAnalytics.cshtml, ProductDetails.cshtml, ThankYou.cshtml). Each file loaded the Coveo analytics script using the standard IIFE loader and called coveoua('init', apiKey) independently.
Here is a quick comparison of the two approaches:
| Aspect | coveo-analytics.js | cshtml Razor Views |
|---|---|---|
| API Key Source | Hardcoded in JS with client-side hostname check | Server-side via Settings.GetSetting("CoveoAnalyticsKey", "") |
| Init Timing | Immediate - runs as soon as script loads | Deferred - waits for CoveoSearchEndpointInitialized event |
| Analytics Routing | Direct to Coveo cloud | Through Sitecore proxy (/coveo/rest/ua/v15) |
| Sends Events | No - just initializes for other components to use | Yes - sends page view with metadata |
On pages where both the JS file and a cshtml view loaded together, things broke. Here are the four problems and fixes.
Problem 1: Analytics Bypassing Sitecore Proxy
The coveo-analytics.js file was sending analytics events directly to analytics.cloud.coveo.com instead of routing through the Sitecore proxy at /coveo/rest/ua/v15. The cshtml files had the proxy override but the JS file did not.
The fix is to override baseUrl on the Coveo analytics client prototype inside the onLoad callback (you need to wait for the script to load before the prototype is available):
coveoua('onLoad', function () {
Object.defineProperty(
coveoanalytics.CoveoAnalyticsClient.prototype,
'baseUrl',
{ get() { return '/coveo/rest/ua/v15'; } }
);
coveoua('init', apiKey);
});
This makes sure all analytics traffic goes through the Sitecore proxy, same as the cshtml views.
Problem 2: InvalidToken from Multiple Initialization
This was the main issue. On pages where both coveo-analytics.js and a cshtml view loaded, coveoua('init') was being called twice. Each call resets the analytics client and creates a new session token. Any events queued from the first initialization become invalid - hence the InvalidToken error.
There was also a secondary issue: the Object.defineProperty call for the proxy override would throw a TypeError on the second call because the property was defined as non-configurable by default.
The fix is a two-flag initialization guard using a shared namespace on window. Why two flags? Because the IIFE script injection is synchronous but the init call happens asynchronously inside onLoad. A single flag would either skip the IIFE or skip the init at the wrong time depending on load order.
window.SITE = window.SITE || {};
// Flag 1: Guard the IIFE script injection (synchronous)
if (!window.SITE._coveoScriptInjected) {
window.SITE._coveoScriptInjected = true;
(function (c, o, v, e, O, u, a) {
a = 'coveoua'; c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
c[a].t = Date.now(); u = o.createElement(v); u.async = 1; u.src = e;
O = o.getElementsByTagName(v)[0]; O.parentNode.insertBefore(u, O)
})(window, document, 'script', 'https://static.cloud.coveo.com/coveo.analytics.js/2/coveoua.js');
}
// Flag 2: Guard the init call (asynchronous, inside onLoad)
coveoua('onLoad', function () {
if (!window.SITE._coveoInitialized) {
window.SITE._coveoInitialized = true;
Object.defineProperty(
coveoanalytics.CoveoAnalyticsClient.prototype,
'baseUrl',
{ get() { return '/coveo/rest/ua/v15'; } }
);
coveoua('init', apiKey);
}
});
This pattern works regardless of which file loads first - JS or cshtml. The first one to run sets the flag and does the initialization. The rest skip it. Event-sending code like coveoua('send', 'view', metadata) or coveoua('ec:addProduct', ...) does not need guards - those use the internal queue and will execute correctly once init completes.
I applied this guard pattern across all four files that had Coveo analytics initialization.
Problem 3: ExpiredToken and InvalidToken from Bot Traffic
While debugging the above, I also noticed two other errors in the Coveo logs that turned out to be caused by Googlebot crawling the site. This is worth calling out because in today's landscape of bots and AI crawlers, bot behavior is constantly changing. Bots render JavaScript, execute search queries, and trigger analytics events just like real users do. If you are not filtering bot traffic from your analytics endpoints, you end up with polluted data - expired tokens from stale bot renders, invalid sessions from crawlers re-initializing your analytics client, and noise that makes it harder to trust your actual user metrics. Keeping bot traffic out of your analytics pipeline is not optional anymore.
The first was a 419 ExpiredToken on the search proxy (/coveo/rest/search/v2). Sitecore generates a search JWT at page render time with a 24-hour TTL. By the time Googlebot crawled and executed the page, the token was expired. This is a server-side issue - the frontend cannot fix it.
The second was a 400 InvalidToken on the analytics proxy (/coveo/rest/ua/v15). This was a different root cause - the CoveoAnalyticsKey Sitecore setting on one of the sites was misconfigured. It had a search JWT (issued by SearchApi, with a queryExecutor role) instead of a static analytics API key (the xx... format). The analytics endpoint does not accept search tokens, so it returned InvalidToken.
Quick check: If your CoveoAnalyticsKey Sitecore setting starts with xx, you are good. If it looks like a long JWT string, it is wrong - you need the static analytics API key from your Coveo admin console.
Filtering Bot Traffic from Analytics
To stop bots from generating noise in your Coveo analytics, you have a few options:
| Approach | How | Notes |
|---|---|---|
| Cloudflare WAF | Block Googlebot on /coveo/rest/ua/* only |
Best option if Cloudflare is in your stack. Do NOT block /coveo/rest/search/ - that will break SEO. |
| IIS URL Rewrite | Match User-Agent containing "Googlebot" on ^coveo/rest/ua/.* and abort |
Works at the server level before Sitecore processes the request |
| JavaScript | Check navigator.userAgent for bot patterns before calling initCoveoUa() |
Secondary layer - Googlebot can execute JS, so not fully reliable |
The key point: only block bots from the analytics path (/coveo/rest/ua/). The search path (/coveo/rest/search/) needs to stay open for Googlebot to index your search-driven content like product pages and knowledge base articles.
And do not just think about Googlebot. Bingbot, SEMrush, Ahrefs, AI training crawlers - they all behave differently, and they change their patterns over time. None of them have any legitimate reason to send commerce analytics events like add-to-cart or purchase. Filtering them out keeps your Coveo ML models trained on real user behavior, not crawler noise.
Problem 4: Google Translate Users Getting Failed Analytics (CORS Preflight)
This one was not a bot issue at all. I noticed 400 errors on OPTIONS /coveo/rest/ua/v15/analytics/custom in IIS logs, with a real browser User-Agent (iPhone Safari) and a Referer from *.translate.goog.
When a user browses your site through Google Translate, the page is served from *.translate.goog (e.g. yourdomain-com.translate.goog). Any JavaScript on that page making requests back to your actual domain is now cross-origin. The browser sends an HTTP OPTIONS preflight request before the actual analytics POST.
Here is the flow:
- User visits
yourdomain-com.translate.goog - JS tries to POST analytics to
yourdomain.com/coveo/rest/ua/v15/analytics/custom - Browser detects cross-origin and sends
OPTIONSpreflight first - Sitecore returns
400(Coveo proxy handler does not handle OPTIONS) - Browser blocks the actual analytics POST
- Commerce events (add-to-cart, purchase, page view) are never recorded for translated sessions
These are real users losing analytics tracking - not bots. The Cloudflare bot-blocking rule from Problem 3 should NOT block this traffic.
Why Not Cloudflare?
I initially considered Cloudflare for this fix, but it does not work well here. Cloudflare Transform Rules can add headers but cannot change the HTTP status code. A WAF Custom Rule with "Return fixed response" can return a 204, but it cannot echo back the dynamic Origin header value - which is required for CORS. The origin changes per site (e.g. example-com.translate.goog), so you cannot hardcode it.
Fix: Sitecore httpRequestBegin Pipeline Processor
The fix that worked is a custom Sitecore httpRequestBegin pipeline processor that intercepts OPTIONS preflight requests for the Coveo analytics path and returns 204 with the correct CORS headers before the request reaches the Coveo proxy handler.
public class HandleCoveoCors : HttpRequestProcessor
{
public override void Process(HttpRequestArgs args)
{
var request = HttpContext.Current.Request;
var response = HttpContext.Current.Response;
var origin = request.Headers["Origin"] ?? "";
if (request.HttpMethod == "OPTIONS" &&
request.Path.StartsWith("/coveo/rest/") &&
origin.EndsWith(".translate.goog"))
{
response.StatusCode = 204;
response.AddHeader("Access-Control-Allow-Origin", origin);
response.AddHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
response.AddHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization");
response.AddHeader("Access-Control-Max-Age", "86400");
response.End();
}
}
}
A few notes on the implementation:
- Why
origin.EndsWith(".translate.goog"): Google Translate generates subdomains dynamically per site. Matching the suffix covers all translated variants without hardcoding each one. - Why
204and not200:204 No Contentis the correct HTTP status for a successful OPTIONS preflight with no response body. Browsers accept both, but 204 is the right semantic choice. - Why pipeline processor: It handles both the status code and the dynamic Origin header correctly, and keeps the logic server-side alongside the Coveo proxy handler it is protecting.
Summary
Four issues, four fixes:
- Analytics bypassing proxy - Add
baseUrloverride viaObject.definePropertyinsideonLoad - InvalidToken from duplicate init - Two-flag initialization guard (
_coveoScriptInjected+_coveoInitialized) on a sharedwindownamespace - Bot traffic causing token errors - Block bots from analytics proxy only (not search), and verify your
CoveoAnalyticsKeySitecore setting has a staticxx...key - Google Translate CORS preflight - Sitecore
httpRequestBeginpipeline processor to handleOPTIONSrequests from*.translate.googwith proper CORS headers
Hope this helps if you are dealing with similar Coveo analytics issues in Sitecore. If you have questions, leave a comment below.