Countly: Self-Hosted Product Analytics with Crash | OpsBlu Docs

Countly: Self-Hosted Product Analytics with Crash

Deploy Countly open-source product analytics with the Countly.q.push() API for custom events, user segmentation, crash reporting, push notifications,...

How Countly Works

Countly is an open-source product analytics platform designed for web, mobile, and desktop applications. It processes events in real time, stores data in MongoDB, and exposes a plugin-based architecture that extends core analytics with crash reporting, push notifications, A/B testing, surveys, and user profiles.

The data collection model uses a lightweight SDK that batches events locally and flushes them to the Countly server at configurable intervals (default: 60 seconds). Each request contains a JSON payload of events, session data, device metrics, and any user properties. The server processes these payloads immediately, making data available in dashboards within seconds of receipt.

Countly's identity model tracks devices by default. Each SDK instance generates a device-level ID stored in localStorage (web) or the device keychain (mobile). When you call change_id with a server-side user ID, Countly merges the device history into the user profile. This merge is server-side and permanent, so a user who logs in from multiple devices gets a unified profile.

The self-hosted deployment runs as a Node.js application backed by MongoDB and optionally Redis for caching. A single Countly server can handle tens of thousands of requests per second. For higher throughput, you deploy multiple API nodes behind a load balancer with a shared MongoDB replica set. The entire stack fits in a single Docker Compose file or scales to a Kubernetes cluster.

Installing the Countly Web SDK

Direct Script Installation

Add the Countly SDK before the closing </head> tag. Replace YOUR_APP_KEY with the app key from your Countly dashboard and https://your-countly-server.com with your server URL.

<script type="text/javascript">
  var Countly = Countly || {};
  Countly.q = Countly.q || [];

  Countly.app_key = 'YOUR_APP_KEY';
  Countly.url = 'https://your-countly-server.com';
  Countly.app_version = '1.0.0';
  Countly.debug = false;

  // Enable features
  Countly.q.push(['track_sessions']);
  Countly.q.push(['track_pageview']);
  Countly.q.push(['track_errors']);
  Countly.q.push(['track_links']);
  Countly.q.push(['track_forms']);
  Countly.q.push(['track_scrolls']);

  // Load the SDK
  (function () {
    var cly = document.createElement('script');
    cly.type = 'text/javascript';
    cly.async = true;
    cly.src = 'https://your-countly-server.com/sdk/web/countly.min.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(cly, s);
  })();
</script>

NPM Installation

For bundled applications (React, Vue, Angular), install via npm:

npm install countly-sdk-web

Initialize in your application entry point:

import Countly from 'countly-sdk-web';

Countly.init({
  app_key: 'YOUR_APP_KEY',
  url: 'https://your-countly-server.com',
  app_version: '1.0.0',
  debug: false
});

Countly.track_sessions();
Countly.track_pageview();
Countly.track_errors();
Countly.track_links();
Countly.track_forms();
Countly.track_scrolls();

Loading via Google Tag Manager

Create a Custom HTML tag with the direct script installation code above. Set it to fire on All Pages. Ensure it fires before any tags that call Countly.q.push().

Verify the installation by checking the Countly dashboard under Analytics > Sessions. Your session should appear within 60 seconds (the default flush interval). You can also set Countly.debug = true temporarily to see SDK activity in the browser console.

Core Tracking Features

Session Tracking

track_sessions automatically records session start, duration, and end events. Countly defines a session as active while the user's browser tab is focused. When the tab loses focus, the session pauses. When the tab regains focus, it resumes. If the tab stays inactive for more than 60 seconds, the session ends and a new one starts on return.

Automatic Pageview Tracking

track_pageview records each page load with the URL, title, referrer, and resolution. For single-page applications, manually trigger pageviews on route changes:

// After client-side navigation
Countly.q.push(['track_pageview', '/dashboard/settings']);

Or with the npm module:

Countly.track_pageview('/dashboard/settings');

track_scrolls records how far visitors scroll on each page (in percentage increments). track_links captures outbound link clicks with the destination URL. Both feed into the Views and Analytics sections of the dashboard.

Form Analytics

track_forms records form submissions with the form ID, action URL, and input field names (values are excluded for privacy). This data appears under Analytics > Forms.

Custom Event Tracking

Countly's event model supports four numerical values per event: count, sum, dur (duration), and segmentation (key-value metadata).

// Basic event
Countly.q.push(['add_event', {
  key: 'button_click',
  count: 1,
  segmentation: {
    button_id: 'cta-hero',
    page: '/pricing'
  }
}]);

// E-commerce purchase event
Countly.q.push(['add_event', {
  key: 'purchase',
  count: 1,
  sum: 149.99,
  segmentation: {
    plan: 'Pro Annual',
    payment_method: 'credit_card',
    currency: 'USD',
    coupon: 'SAVE20'
  }
}]);

// Timed event (measure duration)
Countly.q.push(['start_event', 'video_watch']);

// ... user watches video ...

Countly.q.push(['end_event', {
  key: 'video_watch',
  segmentation: {
    video_id: 'intro-tour',
    completed: true
  }
}]);

The start_event / end_event pair automatically calculates the dur value in seconds. Use this for measuring time-based interactions like video watches, form completion, or feature usage.

Segmentation Best Practices

Segmentation values should be low-cardinality (categories, not unique IDs). Good segmentation keys: plan, device_type, country, feature_name. Avoid high-cardinality values like user IDs, timestamps, or session IDs in segmentation, as these create excessive unique entries in the event index.

User Profiles

Set custom properties on user profiles to enable segmentation and personalized experiences:

// Set standard user properties
Countly.q.push(['user_details', {
  name: 'Jane Martinez',
  email: 'jane@acme.com',
  organization: 'Acme Corp',
  phone: '+1-555-0123',
  picture: 'https://example.com/avatars/jane.jpg',
  gender: 'F',
  byear: 1990,
  custom: {
    plan: 'Enterprise',
    company_size: 150,
    industry: 'SaaS',
    signup_source: 'webinar'
  }
}]);

Update individual custom properties without overwriting the entire profile:

Countly.q.push(['userData.set', 'plan', 'Enterprise']);
Countly.q.push(['userData.set', 'last_login', '2026-03-01']);
Countly.q.push(['userData.increment', 'login_count']);
Countly.q.push(['userData.push', 'features_used', 'dashboard_builder']);
Countly.q.push(['userData.save']);

The userData.save() call flushes all pending property changes to the server.

Crash Reporting

track_errors captures unhandled JavaScript exceptions and unhandled promise rejections. Each crash report includes the error message, stack trace, device info, app version, and breadcrumbs (recent user actions leading to the crash).

Log handled exceptions manually:

try {
  riskyOperation();
} catch (error) {
  Countly.q.push(['log_error', error, {
    context: 'payment_processing',
    user_action: 'clicked_pay_button'
  }]);
}

Add breadcrumbs to provide context for crash reports:

Countly.q.push(['add_log', 'User navigated to /checkout']);
Countly.q.push(['add_log', 'Selected plan: Pro Annual']);
Countly.q.push(['add_log', 'Entered payment details']);
// If a crash happens next, these breadcrumbs appear in the crash report

Crash reports are grouped by stack trace signature. The dashboard shows crash frequency, affected users, and whether a crash is new or recurring.

Integration with Other Tools

Countly REST API

Submit events from your backend for server-side actions like subscription renewals, webhook processing, or batch imports:

curl -X POST "https://your-countly-server.com/i" \
  -d "app_key=YOUR_APP_KEY" \
  -d "device_id=user-12345" \
  -d 'events=[{"key":"subscription_renewed","count":1,"sum":149.99,"segmentation":{"plan":"Pro"}}]'

Forwarding to GA4 via Data Layer

Push Countly events into the GTM data layer for parallel GA4 tracking:

function trackCountlyAndGA4(eventKey, segmentation, sum) {
  Countly.q.push(['add_event', {
    key: eventKey,
    count: 1,
    sum: sum || 0,
    segmentation: segmentation || {}
  }]);

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: 'countly_' + eventKey,
    ...segmentation,
    value: sum || 0
  });
}

trackCountlyAndGA4('purchase', { plan: 'Pro', currency: 'USD' }, 149.99);

Data Export and Warehousing

Countly Enterprise includes a data export plugin that pushes raw event data to external databases. For self-hosted Community edition, query MongoDB directly:

mongosh countly --eval "db.events.find({ 'key': 'purchase' }).limit(10).pretty()"

For production data pipelines, use Countly's export API or set up a MongoDB change stream to feed events into your data warehouse in real time.

Common Errors

Error Cause Fix
No data appears in dashboard app_key does not match the key in Countly dashboard, or url points to wrong server Copy the app key directly from Dashboard > Management > Apps
Countly is not defined SDK script failed to load due to network error, ad blocker, or CSP Add your Countly server domain to CSP script-src and connect-src directives
Sessions show as 0 seconds track_sessions not called, or page unloads before session start event sends Ensure track_sessions is called immediately after init; Countly batches with a 60s interval, so very short visits may not register
Events not appearing Event payload exceeds the server's max_request_size (default 20 KB) Reduce segmentation data size or increase max_request_size in Countly server config
Duplicate events on SPA navigation track_pageview called automatically on init and again manually on route change Call track_pageview with an explicit path on SPA navigation instead of relying on automatic tracking
User profiles not merging after login change_id called without the merge flag set to true Use Countly.change_id('user-id', true) to merge anonymous device data into the user profile
Crash reports missing stack traces Source maps not uploaded, or error originates from a cross-origin script Upload source maps to Countly; add crossorigin="anonymous" to third-party script tags
Push notifications not delivered APNs/FCM credentials expired or misconfigured in Countly server settings Verify push credentials in Dashboard > Management > Apps > Push Notifications
Self-hosted dashboard slow or unresponsive MongoDB indexes missing or insufficient RAM for working set Run countly reindex and ensure MongoDB has enough RAM to hold the hot data set
Data retention filling disk No TTL configured on events collection Set data_retention plugin to auto-purge old data, or configure MongoDB TTL indexes

Performance Considerations

The Countly Web SDK is approximately 35 KB gzipped. It loads asynchronously and uses the Countly.q queue pattern, so you can push events immediately even before the SDK finishes loading.

The default flush interval is 60 seconds. For high-traffic sites, this batching reduces the number of HTTP requests. You can adjust it:

Countly.q.push(['change_id', userId, true]);
// Adjust flush interval (in milliseconds)
Countly.beat_interval = 30; // Flush every 30 seconds

For self-hosted deployments, the primary performance bottleneck is MongoDB write throughput. A single Countly API server can handle approximately 5,000-10,000 events per second depending on hardware. Beyond that, add API nodes behind a load balancer. Use MongoDB replica sets for high availability and sharding for horizontal write scaling.

Countly stores all data in MongoDB, so your disk footprint grows linearly with event volume. A site tracking 10 million events per month uses approximately 5-10 GB of MongoDB storage. Plan your retention policy accordingly, as the Community edition does not include automatic data purging.

For client-side performance, the track_scrolls feature adds a scroll event listener that fires on every scroll frame. On pages with very long content or complex scroll interactions, this can add minor CPU overhead. If you notice scroll jank on specific pages, disable scroll tracking for those URLs.