Menu
Products
Products
Video Hosting
Upload and manage your videos in a centralized video library.
Image Hosting
Upload and manage all your images in a centralized library.
Galleries
Choose from 100+templates to showcase your media in style.
Video Messaging
Record, and send personalized video messages.
CincoTube
Create your own community video hub your team, students or fans.
Pages
Create dedicated webpages to share your videos and images.
Live
Create dedicated webpages to share your videos and images.
For Developers
Video API
Build a unique video experience.
DeepUploader
Collect and store user content from anywhere with our file uploader.
Solutions
Solutions
Enterprise
Supercharge your business with secure, internal communication.
Townhall
Webinars
Team Collaboration
Learning & Development
Creative Professionals
Get creative with a built in-suite of editing and marketing tools.
eCommerce
Boost sales with interactive video and easy-embedding.
Townhall
Webinars
Team Collaboration
Learning & Development
eLearning & Training
Host and share course materials in a centralized portal.
Sales & Marketing
Attract, engage and convert with interactive tools and analytics.
"Cincopa helped my Enterprise organization collaborate better through video."
Book a Demo
Resources
Resources
Blog
Learn about the latest industry trends, tips & tricks.
Help Centre
Get access to help articles FAQs, and all things Cincopa.
Partners
Check out our valued list of partners.
Product Updates
Stay up-to-date with our latest greatest features.
Ebooks, Guides & More
Customer Stories
Hear how we've helped businesses succeed.
Boost Campaign Performance Through Video
Discover how to boost your next campaign by using video.
Download Now
Pricing
Watch a Demo
Demo
Login
Start Free Trial
Strapi’s modular plugin system empowers developers to extend its capabilities beyond content management by integrating custom workflows and specialized data handling. Video analytics require tailored tracking and aggregation of user engagement metrics (such as play events, watch durations, completions) that native CMS features don’t provide out of the box. Embedding video analytics directly within Strapi via custom plugins enables unified data management, real-time insights, and secure, role-based access to video performance metrics essential for optimizing content strategy and user experience. Prerequisites Install Node.js (v14+), Strapi (v4+), and an SQL-based database such as SQLite for dev or PostgreSQL for production environments. Set up the initial Strapi project by running the installation command and creating a video content type with fields such as title (string) and file (media) using strapi generate:content-type video --field title:string --field file:media . Configure the media library in Strapi to handle video uploads and expose basic API endpoints for video retrieval. Creating the Custom Video Analytics Plugin Generate the plugin structure by running npx strapi generate:plugin video-analytics . In your Strapi project root, execute this command to create the plugin scaffold under src/plugins/video-analytics . This generates directories for admin , api , config , controllers , models , routes , and services . Restart the development server ( npm run develop ) to register the plugin, and verify it appears in the admin panel under Plugins (if not, run npm run strapi build for safety). Define the models within the plugin by creating a VideoMetric schema with fields such as videoId (relation to video), userId (UID), eventType (enumeration for play/pause/etc.), timestamp (datetime), and duration (float). Edit src/plugins/video-analytics/models/VideoMetric.settings.json to include: { 'kind': 'collectionType', 'collectionName': 'video_metrics', 'info': { 'singularName': 'video-metric', 'pluralName': 'video-metrics', 'displayName': 'Video Metric' }, 'options': { 'draftAndPublish': false }, 'pluginOptions': {}, 'attributes': { 'videoId': { 'type': 'relation', 'relation': 'manyToOne', 'target': 'api::video.video', 'inversedBy': 'metrics' }, 'userId': { 'type': 'uid' }, 'eventType': { 'type': 'enumeration', 'enum': ['play', 'pause', 'seek', 'end', 'view'] }, 'timestamp': { 'type': 'datetime' }, 'duration': { 'type': 'float' } } } Run npm run develop again to sync the model with the database (or use npm run strapi build for production-like validation). Implement controllers and services by adding API routes like POST /video-analytics/track for logging events and writing service logic to aggregate data using Knex queries for operations like sums and averages. In src/plugins/video-analytics/routes/video-analytics.js , define: module.exports = { routes: [ { method: 'POST', path: '/video-analytics/track', handler: 'video-analytics.track', config: { policies: [] } }, { method: 'GET', path: '/video-analytics/:id/summary', handler: 'video-analytics.summary', config: { policies: [] } } ] }; For the controller ( src/plugins/video-analytics/controllers/video-analytics.js ): 'use strict'; const { createCoreController } = require('@strapi/strapi').factories; const { sanitizeEntity } = require('strapi-utils'); module.exports = createCoreController('plugin::video-analytics.video-metric', ({ strapi }) => ({ async track(ctx) { try { const { videoId, userId, eventType, duration } = ctx.request.body; // Validation (detailed in next section) if (!['play', 'pause', 'seek', 'end', 'view'].includes(eventType) || !videoId || !userId) { return ctx.badRequest('Invalid event data'); } // Rate limiting (detailed in next section) const recentEvents = await strapi.entityService.findMany('plugin::video-analytics.video-metric', { filters: { userId, timestamp: { gt: new Date(Date.now() - 60000).toISOString() } }, _limit: 10 }); if (recentEvents.length >= 10) return ctx.badRequest('Rate limit exceeded'); const metric = await strapi.entityService.create('plugin::video-analytics.video-metric', { data: { videoId, userId, eventType, timestamp: new Date().toISOString(), duration } }); ctx.body = { success: true, id: metric.id }; } catch (error) { ctx.badRequest('Error tracking event', { error: error.message }); } }, async summary(ctx) { try { const { id } = ctx.params; const { startDate, endDate, eventType } = ctx.query; const filters = { videoId: id }; if (startDate) filters.timestamp = { gte: new Date(startDate).toISOString() }; if (endDate) filters.timestamp = { ...filters.timestamp, lte: new Date(endDate).toISOString() }; if (eventType) filters.eventType = eventType; const metrics = await strapi.entityService.findMany('plugin::video-analytics.video-metric', { filters, populate: ['videoId'], sort: { timestamp: 'desc' } }); const avgWatchTime = await strapi.plugin('video-analytics').service('video-metric').calculateAverageWatchTime(id); ctx.body = { metrics: metrics.map(entity => sanitizeEntity(entity, { model: strapi.getModel('plugin::video-analytics.video-metric') }) || [], averageWatchTime }; } catch (error) { ctx.badRequest('Error fetching summary', { error: error.message }); } } })); Create a service in src/plugins/video-analytics/services/video-metric.js for reusable logic, e.g., aggregation functions (note: service name matches model plural): 'use strict'; const { createCoreService } = require('@strapi/strapi').factories; module.exports = createCoreService('plugin::video-analytics.video-metric', ({ strapi }) => ({ async calculateAverageWatchTime(videoId) { const db = strapi.db.connection; const result = await db('video_metrics') .where({ videoId, eventType: 'end' }) .avg('duration as avg_duration') .first(); return parseFloat(result.avg_duration) || 0; } })); Integrate middleware by hooking into Strapi's request lifecycle to enable automatic tracking on video access events. In src/plugins/video-analytics/middlewares/track-video.js , define: module.exports = (config, { strapi }) => { return async (ctx, next) => { // Match single video GET requests (e.g., /api/videos/1) if (ctx.path.match(/^\/api\/videos\/\d+$/) && ctx.method === 'GET') { const videoId = ctx.path.split('/').pop(); // Extract ID from path const userId = ctx.state.user?.id || 'anonymous'; // From JWT if authenticated try { await strapi.entityService.create('plugin::video-analytics.video-metric', { data: { videoId, userId, eventType: 'view', timestamp: new Date().toISOString() } }); } catch (error) { strapi.log.warn('Failed to log view event:', error); } } await next(); }; }; Register it in src/plugins/video-analytics/config/middlewares.js : module.exports = { TrackVideo: true }; (this enables it for the plugin; for global use, add to the main app's config/middlewares.js as module.exports = ['plugin::video-analytics.track-video'];) . Implementing Analytics Tracking Integrate on the frontend by adding JavaScript event listeners, such as video.addEventListener('timeupdate', sendToStrapi) to capture and transmit video events. In your frontend app (e.g., a Vue or React component), use refs for scoping and debounce frequent events like timeupdate (e.g., every 5 seconds) to avoid overload. Example in Vanilla JS or Framework-Agnostic : // Debounce utility function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const video = document.querySelector('video'); // Or use ref in Vue/React const strapiUrl = 'http://localhost:1337'; const token = 'your-jwt-token'; // From auth, if protected const sendEvent = debounce(async (eventType, duration = 0) => { try { await fetch(`${strapiUrl}/api/video-analytics/track`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` // If endpoints require auth }, body: JSON.stringify({ videoId: 'your-video-id', // From props, URL, or data attribute userId: 'user-123', // From auth session eventType, duration }) }); } catch (error) { console.error('Tracking failed:', error); } }, 5000); // Debounce 5s for timeupdate video.addEventListener('play', () => sendEvent('play')); video.addEventListener('pause', () => sendEvent('pause')); video.addEventListener('timeupdate', () => sendEvent('seek', video.currentTime)); video.addEventListener('ended', () => sendEvent('end', video.duration)); Process on the backend by validating incoming events in the plugin controller, storing them in the database, and applying rate limiting to prevent abuse. The track method above includes validation (enum check, required fields) and basic rate limiting (last-minute events per user). For advanced rate limiting, install @strapi/plugin-users-permissions for auth and use a dedicated plugin like strapi-plugin-rate-limit . Sanitize inputs with strapi.utils.sanitize if extending validation. Develop querying endpoints by creating custom API routes like GET /video-analytics/:id/summary that support filters for date ranges and metric types. The summary method above handles this with corrected filters (e.g., gte / lte operators, ISO strings) and populates relations. It integrates the service for average watch time and sanitizes output for security. Include example code snippets such as a plugin controller method for event ingestion and a service function for calculating metrics like average watch time. See the controller track method and service calculateAverageWatchTime above. To call the service in other parts: const avg = await strapi.plugin('video-analytics').service('video-metric').calculateAverageWatchTime(id); . Visualization and Admin Extensions Extend the admin panel by adding a dashboard widget using React components and integrating Chart.js to render graphs for analytics data. Install Chart.js in the project root ( npm install chart.js react-chartjs-2 ). In src/plugins/video-analytics/admin/src/components/VideoAnalyticsDashboard/index.js , create: import React, { useEffect, useState } from 'react'; import { Bar } from 'react-chartjs-2'; import { Chart as ChartJS, CategoryScale, LinearScale, BarElement } from 'chart.js'; import { request } from '@strapi/helper-plugin'; // Strapi's request util for auth ChartJS.register(CategoryScale, LinearScale, BarElement); const VideoAnalyticsDashboard = ({ videoId = 1 }) => { // Make dynamic const [data, setData] = useState({ labels: [], datasets: [] }); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { try { const summary = await request(`/video-analytics/${videoId}/summary`, { method: 'GET' }); // Auto-includes auth token const labels = [...new Set(summary.metrics.map(m => m.eventType))]; // Unique events const counts = {}; summary.metrics.forEach(m => { counts[m.eventType] = (counts[m.eventType] || 0) + 1; }); setData({ labels, datasets: [{ label: 'Event Counts', data: labels.map(label => counts[label]), backgroundColor: 'rgba(75,192,192,0.6)' }] }); } catch (error) { console.error('Failed to fetch analytics:', error); } finally { setLoading(false); } }; fetchData(); }, [videoId]); if (loading) return
Loading analytics...
; return
; }; export default VideoAnalyticsDashboard; Register it in src/plugins/video-analytics/admin/src/index.js by injecting into the admin app (e.g., add a menu link): export default { register(app) { app.injectContentManagerComponent('list', 'video', { name: 'video-analytics', Component: async () => import('./components/VideoAnalyticsDashboard') }); // Or for a custom menu: app.addMenuLink({ to: '/plugins/video-analytics', icon: VideoIcon, intlLabel: { id: 'video-analytics.menu', defaultMessage: 'Video Analytics' }, Component: () => import('./pages/App') }); }, bootstrap(app) {} }; Expose data for external use by providing aggregated analytics through GraphQL queries or REST endpoints. Install the GraphQL plugin ( npm run strapi install graphql ), which auto-generates schemas from your models. Query example (via GraphQL playground at /graphql ): { videoMetrics(filters: { videoId: { eq: '1' } }) { data { attributes { eventType duration timestamp } } } } . For custom resolvers, extend in src/plugins/video-analytics/server/graphql/resolvers.js if needed. Secure access by implementing role-based controls, such as restricting full analytics views to admin users only. In the admin panel, go to Settings > Users & Permissions > Roles > Authenticated (or create a custom role), and enable permissions for the plugin's routes (e.g., find “ video-analytics ” under Application > video-metric and toggle Create/Read/Update). For finer control, add policies in routes: config: { policies: ['global::is-admin'] } . Define the policy in src/policies/is-admin.js : module.exports = async (ctx, next) => { if (ctx.state.user?.role?.type !== 'admin') { return ctx.unauthorized('Admin access required'); } return await next(); }; Testing and Deployment Write unit and integration tests using Jest to cover services and controllers, including mocks for video events. Install Jest ( npm install --save-dev jest @strapi/test-utils ), add 'test': 'jest' to package.json , then create src/plugins/video-analytics/__tests__/video-metric.test.js (match service name): const { createStrapiInstance } = require('@strapi/test-utils'); describe('Video Metric Service', () => { let strapi; beforeAll(async () => { strapi = await createStrapiInstance({ dir: __dirname, env: 'test', autoReload: true }); await strapi.load(); }); afterAll(async () => { await strapi.stop(); }); test('calculates average watch time', async () => { // Mock DB query if needed, or seed test data const service = strapi.plugin('video-analytics').service('video-metric'); const avg = await service.calculateAverageWatchTime('test-video-id'); expect(avg).toBeGreaterThanOrEqual(0); }); test('tracks event (mocked)', async () => { const mockCreate = jest.spyOn(strapi.entityService, 'create').mockResolvedValue({ id: 1 }); const service = strapi.plugin('video-analytics').service('video-metric'); const result = await service.create({ data: { eventType: 'play', videoId: '1', userId: 'user-1' } }); expect(mockCreate).toHaveBeenCalled(); expect(result).toHaveProperty('id'); mockCreate.mockRestore(); }); }); Run with npm test . For integration, use Strapi's test DB and seed video metrics. Address performance by indexing relevant database fields and implementing query caching with tools like Redis. Use Strapi's migration system: Create src/database/migrations/20230101-add-index.js with module.exports = { async up(queryInterface) { await queryInterface.sequelize.query('CREATE INDEX idx_video_metrics_videoId ON video_metrics (videoId);'); } }; and run npm run strapi db:migrate . For caching, install Redis ( npm install redis ), create a client in a service (e.g., const redis = require('redis').createClient({ url: process.env.REDIS_URL }); await redis.connect();), and wrap queries: async getCachedSummary(key, callback)