These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Coveo Analytics?
Coveo Analytics (also called Coveo Usage Analytics) is an event tracking and analytics service that collects user interaction data from search implementations. The platform tracks search queries, click events, content views, and custom user actions.
This behavioral data feeds machine learning models that automatically improve search relevance, personalize content recommendations, and provide insights into user behavior patterns. Coveo provides JavaScript SDKs and REST APIs for implementing analytics across different application architectures.
Why Integrate Coveo Analytics with Strapi
Combining Coveo Analytics with Strapi's headless CMS capabilities addresses several challenges full-stack developers face when building content-driven applications:
- API-Driven Search Architecture: Coveo indexes Strapi content through REST API sources, eliminating custom search infrastructure while integrating naturally with Strapi's API-first approach.
- Automated Relevance Improvements: User interactions automatically train Coveo's machine learning models, improving search relevance over time. No manual algorithm tuning or custom ML pipelines required.
- Real-Time Content Synchronization: Strapi's webhook system triggers Coveo index updates when content changes, maintaining search freshness automatically.
- Developer-Controlled Personalization: The Coveo Headless library provides framework-agnostic state management (React, Vue, Angular, vanilla JavaScript) that aligns with Strapi's plugin architecture for custom integrations.
How to Integrate Coveo Analytics with Strapi
Implementation Note: The code examples in this tutorial demonstrate architectural patterns for integrating Coveo Analytics with Strapi. Methods, API endpoints, and field structures may vary based on your Coveo SDK version and organization configuration.
Setting up analytics tracking is often the first roadblock—getting the SDK properly initialized without breaking existing request flows requires finesse. This implementation uses Strapi's middleware system and lifecycle hooks to capture user interactions while maintaining application performance.
Prerequisites
Before diving in, you need:
- A Strapi 5 instance running Node.js 18 or higher.
- A Coveo organization account with API access.
- An API key with Analytics Data privileges.
- Familiarity with Strapi's backend customization and plugin system.
Install the Coveo Analytics SDK
Start by installing the Coveo Analytics JavaScript library:
npm install coveo.analytics.jsThis installs the coveo.analytics.js package, which provides the Coveo Analytics JavaScript library for Node.js applications. For TypeScript projects, the package includes type definitions automatically.
Build a Custom Analytics Service
Scattering analytics calls throughout your codebase is a maintenance nightmare that comes back to haunt you during upgrades. Centralizing this logic in a service saves hours of debugging later. Create a custom service in Strapi to encapsulate Coveo Analytics interactions—making analytics logic reusable across controllers and lifecycle hooks.
Create a dedicated service file at src/services/coveo-analytics.js:
const coveoua = require('coveo.analytics');
module.exports = ({ strapi }) => ({
client: null,
async initialize() {
// Initialize Coveo Analytics client using the SDK
this.client = coveoua('init', process.env.COVEO_API_KEY, process.env.COVEO_ANALYTICS_ENDPOINT);
strapi.log.info('Coveo Analytics configuration loaded');
},
async trackSearch(userId, query, resultsCount) {
if (!this.client) {
await this.initialize();
}
try {
await this.client('send', 'search', {
searchQueryUid: this.generateQueryId(),
queryText: query,
actionCause: 'searchboxSubmit',
numberOfResults: resultsCount,
responseTime: 0,
language: 'en',
originLevel1: 'StrapiSearchInterface',
anonymous: !userId,
clientId: userId || 'anonymous',
});
} catch (error) {
strapi.log.error('Coveo search tracking failed:', error);
}
},
async trackClick(userId, contentId, contentType, rank) {
if (!this.client) {
await this.initialize();
}
try {
await this.client('send', 'click', {
documentUri: `strapi://${contentType}/${contentId}`,
documentUriHash: this.hashUri(`${contentType}/${contentId}`),
actionCause: 'documentOpen',
searchQueryUid: this.generateQueryId(),
documentPosition: rank,
clientId: userId || 'anonymous',
});
} catch (error) {
strapi.log.error('Coveo click tracking failed:', error);
}
},
async trackCustomEvent(eventName, userId, metadata) {
if (!this.client) {
await this.initialize();
}
try {
await this.client('send', 'customEvent', {
eventType: eventName,
eventValue: eventName,
language: 'en',
customData: metadata,
clientId: userId || 'anonymous',
});
} catch (error) {
strapi.log.error('Coveo custom event tracking failed:', error);
}
},
generateQueryId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
},
hashUri(uri) {
// Simple hash function for URI
let hash = 0;
for (let i = 0; i < uri.length; i++) {
const char = uri.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString();
},
});Register this service in src/index.js:
module.exports = {
async bootstrap({ strapi }) {
await strapi.service('api::coveo-analytics.coveo-analytics').initialize();
strapi.log.info('Analytics services initialized');
},
};Track Content Changes with Lifecycle Hooks
You can use Strapi lifecycle hooks to track content operations without modifying core business logic. These hooks execute at specific points in the content processing workflow.
Implement lifecycle tracking in src/api/article/content-types/article/lifecycles.js:
module.exports = {
async afterCreate(event) {
const { result, params } = event;
// Track article creation in analytics service
await strapi.service('api::analytics.analytics').trackEvent({
event: 'article_created',
articleId: result.id,
contentType: 'article',
title: result.title,
category: result.category,
userId: params.data.createdBy,
timestamp: new Date(),
});
},
async afterUpdate(event) {
const { result, params } = event;
// Track publication events
if (result.publishedAt && !params.data.publishedAt) {
const analyticsService = strapi.service('api::analytics.analytics');
await analyticsService.track(
params.data.createdBy,
'content_published',
{
contentId: result.id,
title: result.title,
publishedAt: result.publishedAt,
category: result.category,
}
);
}
},
};Capture API Usage with Middleware
The tricky part with middleware-based tracking is preventing analytics failures from breaking your API responses. This is where non-blocking patterns become critical. Custom middleware enables request-level analytics tracking for API usage patterns.
Create the middleware in src/middlewares/analytics-tracker.js:
module.exports = (config, { strapi }) => {
return async (ctx, next) => {
const startTime = Date.now();
await next();
const duration = Date.now() - startTime;
// Track API usage patterns
if (ctx.request.path.startsWith('/api/')) {
const analyticsService = strapi.service('api::coveo-analytics.coveo-analytics');
setImmediate(async () => {
try {
await analyticsService.trackCustomEvent(
'api_request',
ctx.state.user?.id,
{
path: ctx.request.path,
method: ctx.request.method,
statusCode: ctx.response.status,
responseTime: duration,
userAgent: ctx.request.headers['user-agent'],
}
);
} catch (error) {
strapi.log.error('Analytics middleware error:', error);
}
});
}
};
};Register the middleware in config/middlewares.js:
module.exports = [
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::logger',
'strapi::query',
'strapi::body',
'strapi::session',
'strapi::favicon',
'strapi::public',
{
name: 'global::analytics-tracker',
config: {},
},
];Track Search Behavior in Controllers
Update your article controller to track search and view analytics. This works with Strapi's controller architecture:
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::article.article', ({ strapi }) => ({
async find(ctx) {
const analyticsService = strapi.service('api::coveo-analytics.coveo-analytics');
// Track search query
const query = ctx.query.filters?.title?.$contains || '';
const sanitizedQuery = await this.sanitizeQuery(ctx);
const { results, pagination } = await strapi
.service('api::article.article')
.find(sanitizedQuery);
// Log search to Coveo
if (query) {
await analyticsService.trackSearch(
ctx.state.user?.id,
query,
results.length
);
}
const sanitizedResults = await this.sanitizeOutput(results, ctx);
return this.transformResponse(sanitizedResults, { pagination });
},
async findOne(ctx) {
const { id } = ctx.params;
const sanitizedQuery = await this.sanitizeQuery(ctx);
const entity = await strapi
.service('api::article.article')
.findOne(id, sanitizedQuery);
const analyticsService = strapi.service('api::analytics.analytics');
// Track article view
await analyticsService.trackEvent({
event: 'article_viewed',
articleId: id,
userId: ctx.state.user?.id,
referrer: ctx.request.headers.referer,
timestamp: new Date(),
});
const sanitizedEntity = await this.sanitizeOutput(entity, ctx);
return this.transformResponse(sanitizedEntity);
},
}));Configuring Environment Variables
Add your Coveo credentials to .env:
COVEO_API_KEY=your_coveo_api_key_here
COVEO_ANALYTICS_ENDPOINT=https://your-org-id.analytics.org.coveo.comNever commit API keys to version control. For production deployments, use Strapi's environment configuration management to securely store credentials.
Implementing Real-Time Content Synchronization
To keep search indexes synchronized with content changes, configure webhooks to notify Coveo when content changes. Create a custom route for webhook handling:
// src/api/article/routes/custom-routes.js
module.exports = {
routes: [
{
method: 'POST',
path: '/articles/sync-to-coveo',
handler: 'article.syncToCoveo',
config: {
auth: false,
},
},
],
};Create the controller method:
async syncToCoveo(ctx) {
const { id } = ctx.request.body;
const article = await strapi.entityService.findOne('api::article.article', id, {
populate: ['author', 'category'],
});
if (!article) {
return ctx.notFound('Article not found');
}
// Send content to Coveo Push API
try {
const response = await fetch(
`${process.env.COVEO_PUSH_API_ENDPOINT}/sources/${process.env.COVEO_SOURCE_ID}/documents?updateSourceStatus=false`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.COVEO_PUSH_API_KEY}`,
},
body: JSON.stringify({
documentId: `article-${id}`,
title: article.title,
body: article.content,
author: article.author?.name,
category: article.category?.name,
date: article.publishedAt,
clickableUri: `${process.env.FRONTEND_URL}/articles/${article.slug}`,
}),
}
);
if (!response.ok) {
const errorData = await response.json();
strapi.log.error('Coveo API error response:', {
statusCode: response.status,
statusText: response.statusText,
error: errorData
});
throw new Error(`Coveo API error: ${response.statusText}`);
}
ctx.send({ success: true });
} catch (error) {
strapi.log.error('Coveo sync error:', error);
ctx.badRequest('Failed to sync with Coveo');
}
}Production Considerations: Implement exponential backoff retry logic for transient Coveo API failures. For rate limit handling patterns, consult the Coveo Push API documentation for burst limit troubleshooting.
This implementation provides a foundation for tracking user behavior through event logging (search, click, view, and custom events), synchronizing content via Strapi's REST APIs or GraphQL APIs, and gathering analytics data that feeds Coveo's machine learning models for continuous relevance improvement. The modular service architecture keeps analytics logic maintainable and testable.
Testing Your Analytics Integration
Testing analytics integrations ensures events are tracked correctly before deployment. Implement these testing strategies to validate your Coveo Analytics implementation:
Unit Testing Analytics Services
Test your analytics service methods with mocked HTTP calls to prevent actual API requests during test execution:
// tests/unit/coveo-analytics.test.js
const coveoAnalyticsService = require('../../src/services/coveo-analytics');
describe('Coveo Analytics Service', () => {
let service;
let mockStrapi;
beforeEach(() => {
mockStrapi = {
log: {
info: jest.fn(),
error: jest.fn(),
},
};
service = coveoAnalyticsService({ strapi: mockStrapi });
});
test('trackSearch sends correct payload structure', async () => {
const mockClient = jest.fn().mockResolvedValue({});
service.client = mockClient;
await service.trackSearch('user123', 'test query', 10);
expect(mockClient).toHaveBeenCalledWith('send', 'search',
expect.objectContaining({
queryText: 'test query',
numberOfResults: 10,
actionCause: 'searchboxSubmit',
clientId: 'user123',
})
);
});
test('trackClick handles errors gracefully', async () => {
const mockClient = jest.fn().mockRejectedValue(new Error('Network error'));
service.client = mockClient;
await service.trackClick('user123', '1', 'article', 1);
expect(mockStrapi.log.error).toHaveBeenCalledWith(
'Coveo click tracking failed:',
expect.any(Error)
);
});
});Integration Testing Lifecycle Hooks
Test lifecycle hooks to verify analytics events trigger correctly during content operations:
// tests/integration/article-lifecycle.test.js
const { setupStrapi, cleanupStrapi } = require('./helpers/strapi');
describe('Article Lifecycle Analytics', () => {
let strapi;
beforeAll(async () => {
strapi = await setupStrapi();
});
afterAll(async () => {
await cleanupStrapi(strapi);
});
test('afterCreate triggers analytics event', async () => {
const analyticsService = strapi.service('api::coveo-analytics.coveo-analytics');
const trackCustomEventSpy = jest.spyOn(analyticsService, 'trackCustomEvent');
await strapi.entityService.create('api::article.article', {
data: {
title: 'Test Article',
content: 'Test content',
},
});
expect(trackCustomEventSpy).toHaveBeenCalledWith(
'article_created',
expect.any(String),
expect.objectContaining({
title: 'Test Article',
})
);
});
});Validating Events in Development
Use Coveo's browser-based validation tools to verify events in development environments:
- Browser DevTools Network Tab: Monitor requests to
analytics.org.coveo.comto inspect event payloads - Coveo Administration Console: Check the Event Errors dashboard for validation failures
- Usage Analytics Dimensions Report: Verify custom metadata appears correctly in analytics reports
Testing in Staging Environments
Before production deployment, test analytics in a staging environment that mirrors production configuration:
// config/env/staging/server.js
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
app: {
keys: env.array('APP_KEYS'),
},
// Use Coveo test/sandbox organization for staging
coveoAnalytics: {
apiKey: env('COVEO_STAGING_API_KEY'),
endpoint: env('COVEO_STAGING_ENDPOINT'),
},
});According to Coveo's data validation documentation, validate these aspects before production:
- Event payload structure correctness
- User journey completeness across multiple interactions
- Data completeness for all required fields
- Custom metadata accuracy
Project Example: Content Discovery Dashboard
Building a content discovery dashboard demonstrates how Strapi and Coveo work together to surface trending content based on user behavior patterns. This project uses Coveo Analytics to track article views and clicks, analyzes user interaction patterns through Coveo's Usage Analytics API, and surfaces popular content through a custom Strapi API endpoint that aggregates trending metrics.
Architecture Overview
The dashboard integrates three key technical components:
- Strapi Backend: Manages article content and exposes REST APIs.
- Coveo Analytics: Collects and tracks user interactions including search queries, clicks, and custom events.
- Frontend Dashboard: Displays analytics data through custom visualizations, consuming data from the analytics platform.
Create an Analytics Content Type
First, implement analytics event tracking using Strapi's lifecycle hooks to capture user interactions when content is created, updated, or deleted. Hook into the appropriate lifecycle methods (afterCreate, afterUpdate, afterDelete) to send analytics data to your chosen external analytics service like Coveo, reducing latency using non-blocking API calls with setImmediate.
Create the content type through Strapi's Content-Type Builder or define it programmatically in src/api/analytics-summary/content-types/analytics-summary/schema.json:
{
"kind": "collectionType",
"collectionName": "analytics_summaries",
"info": {
"singularName": "analytics-summary",
"pluralName": "analytics-summaries",
"displayName": "Analytics Summary",
"description": "Aggregated analytics data from Coveo"
},
"options": {
"draftAndPublish": false
},
"attributes": {
"contentId": {
"type": "string",
"required": true,
"unique": true
},
"contentType": {
"type": "string",
"required": true
},
"viewCount": {
"type": "integer",
"default": 0
},
"clickCount": {
"type": "integer",
"default": 0
},
"searchAppearances": {
"type": "integer",
"default": 0
},
"averageClickPosition": {
"type": "decimal"
},
"trendingScore": {
"type": "decimal"
},
"lastCalculated": {
"type": "datetime"
}
}
}Build the Analytics Aggregation Service
Create a service that fetches analytics data from Coveo and calculates trending scores:
// src/api/analytics-summary/services/analytics-summary.js
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::analytics-summary.analytics-summary', ({ strapi }) => ({
async aggregateAnalytics() {
const coveoService = strapi.service('api::coveo-analytics.coveo-analytics');
try {
// Note: This implementation requires the Coveo Usage Analytics Read API
// to fetch aggregated analytics data. Consult the Coveo Platform documentation
// for the specific endpoint and authentication method for your organization.
// The endpoint structure varies based on your Coveo organization configuration.
// Example pattern (adjust for your organization):
// const response = await fetch(
// `https://platform.cloud.coveo.com/rest/ua/v15/analytics/visits`,
// {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${process.env.COVEO_API_KEY}`,
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({
// // Query parameters for the analytics data you want to retrieve
// })
// }
// );
strapi.log.info('Analytics aggregation method requires Coveo-specific configuration');
} catch (error) {
strapi.log.error('Analytics aggregation failed:', error);
}
},
// Trending algorithm - adjust weights based on your analytics goals
calculateTrendingScore({ viewCount, clickCount, searchAppearances, averageClickPosition }) {
const clickWeight = 0.4;
const viewWeight = 0.3;
const appearanceWeight = 0.2;
const positionWeight = 0.1;
// Normalize position (lower position = better)
const normalizedPosition = averageClickPosition ? (1 / averageClickPosition) * 10 : 0;
return (
(clickCount * clickWeight) +
(viewCount * viewWeight) +
(searchAppearances * appearanceWeight) +
(normalizedPosition * positionWeight)
);
},
async getTrendingContent(limit = 10) {
const summaries = await strapi.entityService.findMany(
'api::analytics-summary.analytics-summary',
{
sort: { trendingScore: 'desc' },
limit,
}
);
// Fetch full content for each trending item
const trendingContent = await Promise.all(
summaries.map(async (summary) => {
const content = await strapi.entityService.findOne(
`api::${summary.contentType}.${summary.contentType}`,
summary.contentId,
{
populate: ['author', 'category'],
}
);
return {
...content,
analytics: {
viewCount: summary.viewCount,
clickCount: summary.clickCount,
trendingScore: summary.trendingScore,
},
};
})
);
return trendingContent;
},
}));Implementation Note: The aggregateAnalytics() method provides a framework for fetching analytics data from Coveo's Usage Analytics Read API. The specific endpoint, request structure, and response format depend on your Coveo organization configuration and should be adapted based on the Coveo Usage Analytics API documentation.
The method demonstrates the integration pattern while acknowledging that implementation details vary by organization.
Create the Dashboard API Endpoint
Build a custom controller to expose dashboard analytics:
// src/api/analytics-summary/controllers/analytics-summary.js
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::analytics-summary.analytics-summary', ({ strapi }) => ({
async dashboard(ctx) {
const analyticsService = strapi.service('api::analytics-summary.analytics-summary');
const [trending, recentSummaries] = await Promise.all([
analyticsService.getTrendingContent(10),
strapi.entityService.findMany('api::analytics-summary.analytics-summary', {
sort: { lastCalculated: 'desc' },
limit: 50,
}),
]);
// Calculate aggregate metrics
const totalViews = recentSummaries.reduce((sum, s) => sum + s.viewCount, 0);
const totalClicks = recentSummaries.reduce((sum, s) => sum + s.clickCount, 0);
const averageCTR = totalViews > 0 ? (totalClicks / totalViews) * 100 : 0;
return {
trending,
metrics: {
totalViews,
totalClicks,
averageCTR: averageCTR.toFixed(2),
contentTracked: recentSummaries.length,
},
lastUpdated: recentSummaries[0]?.lastCalculated,
};
},
async triggerAggregation(ctx) {
const analyticsService = strapi.service('api::analytics-summary.analytics-summary');
// Trigger aggregation in background
setImmediate(() => {
analyticsService.aggregateAnalytics();
});
return { message: 'Aggregation started' };
},
}));Add custom routes in src/api/analytics-summary/routes/custom-routes.js:
module.exports = {
routes: [
{
method: 'GET',
path: '/analytics/dashboard',
handler: 'analytics-summary.dashboard',
config: {
auth: false,
},
},
{
method: 'POST',
path: '/analytics/aggregate',
handler: 'analytics-summary.triggerAggregation',
config: {
policies: ['admin::isAuthenticated'],
},
},
],
};Schedule Automated Analytics Updates
Implement scheduled analytics aggregation using Strapi's plugin system with custom services and middleware. You can create a custom Strapi plugin that registers lifecycle hooks to trigger analytics event aggregation at scheduled intervals.
Note: This example uses Node.js's built-in setInterval for demonstration. For production deployments, consider using dedicated job scheduling solutions like node-cron, Bull, or external cron services for more robust scheduling.
// src/index.js - Add to bootstrap method
module.exports = {
async bootstrap({ strapi }) {
// Initialize analytics service
await strapi.service('api::coveo-analytics.coveo-analytics').initialize();
// Schedule analytics aggregation using setInterval
setInterval(async () => {
try {
await strapi.service('api::analytics-summary.analytics-summary').aggregateAnalytics();
strapi.log.info('Scheduled analytics aggregation completed');
} catch (error) {
strapi.log.error('Scheduled aggregation failed:', error);
}
}, 6 * 60 * 60 * 1000);
strapi.log.info('Analytics services initialized with scheduled aggregation');
},
};Consume Dashboard Data from Your Frontend
Your frontend application can now fetch trending content and analytics metrics:
// Frontend example (Next.js, React, Vue, etc.)
async function fetchDashboard() {
const response = await fetch('http://localhost:1337/api/analytics/dashboard');
const data = await response.json();
console.log('Trending content:', data.trending);
console.log('Metrics:', data.metrics);
console.log('Last updated:', data.lastUpdated);
}This dashboard surfaces insights into content performance by combining Strapi's content management with Coveo's behavioral analytics. The trending score algorithm surfaces content that resonates with users, while the aggregation service keeps dashboard data current.
The architecture demonstrates how Strapi's plugin system, lifecycle hooks, custom middleware, and service integrations enable complex analytics integrations while maintaining a clean separation of concerns.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and Coveo documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.