@deriv-com/analytics
Version:
Comprehensive analytics package for Deriv applications. Provides unified event tracking, A/B testing, and user analytics through RudderStack, PostHog and GrowthBook integrations with built-in caching and offline support.
1,010 lines (752 loc) β’ 33 kB
Markdown
# @deriv-com/analytics
A modern, tree-shakeable analytics library for tracking user events with RudderStack and PostHog. Designed for optimal performance with advanced caching, batching, and offline support.
## Features
- π **Multi-Provider Support**: RudderStack for event tracking and PostHog for analytics & session recording
- π **Tree-Shakeable**: Only bundle what you use - each provider can be imported independently
- π‘ **Offline-First**: Automatic event caching when offline with replay on reconnection
- β‘ **Performance Optimized**: Batching, deduplication, and SendBeacon API for fast tracking
- π **Backward Compatible**: Supports older React, Node.js, and other legacy package versions
- πΎ **Advanced Caching**: localStorage and in-memory caching for robust event delivery
- π₯ **Session Recording**: Built-in PostHog session recording with customizable settings
> **Note**: GrowthBook support is deprecated and will be removed in a future major version. For A/B testing and feature flags, we recommend using PostHog's built-in feature flag capabilities.
## Table of Contents
- [Installation](#installation)
- [NPM/Yarn](#npmyarn)
- [Browser (CDN)](#browser-cdn)
- [Quick Start](#quick-start)
- [Framework Integration](#framework-integration)
- [React](#react-integration)
- [Next.js](#nextjs-integration)
- [Vue.js](#vuejs-integration)
- [Vanilla JavaScript](#vanilla-javascript)
- [Configuration](#configuration)
- [RudderStack](#rudderstack-configuration)
- [PostHog](#posthog-configuration)
- [Enforced settings](#enforced-settings)
- [Overridable defaults](#overridable-defaults)
- [Do not capture $pageview manually](#-do-not-capture-pageview-manually)
- [Domain allowlist](#domain-allowlist)
- [Core API](#core-api)
- [Initialization](#initialization)
- [Event Tracking](#event-tracking)
- [User Identification](#user-identification)
- [Page Views](#page-views)
- [User Attributes](#user-attributes)
- [Caching & Offline Support](#caching--offline-support)
- [Debug Mode](#debug-mode)
- [Advanced Usage](#advanced-usage)
- [PostHog Feature Flags](#posthog-feature-flags)
- [PostHog Integration Checklist](#posthog-integration-checklist)
- [API Reference](#api-reference)
- [Performance](#performance)
- [Troubleshooting](#troubleshooting)
- [Migration Guide](#migration-guide)
## Installation
### NPM/Yarn
```bash
# Using npm
npm install @deriv-com/analytics
# Using yarn
yarn add @deriv-com/analytics
# Using pnpm
pnpm add @deriv-com/analytics
```
**Core dependencies** (`@rudderstack/analytics-js`, `js-cookie`, and `posthog-js`) are installed automatically.
### Browser (CDN)
Use directly in browsers without a build tool:
```html
<!-- Load from jsdelivr CDN -->
<script src="https://cdn.jsdelivr.net/npm/@deriv-com/analytics@latest/dist/browser/analytics.bundle.global.js"></script>
<script>
const { Analytics } = window.DerivAnalytics
Analytics.initialise({
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
posthogOptions: {
apiKey: 'YOUR_POSTHOG_KEY',
config: {
autocapture: true,
},
},
}).then(() => {
Analytics.trackEvent('page_view', { page: 'home' })
})
</script>
```
**Bundle Size**: ~380 KB minified / ~125 KB gzipped (includes RudderStack + PostHog + all dependencies)
## Quick Start
### Basic Usage (RudderStack Only)
```typescript
import { Analytics } from '@deriv-com/analytics'
// Initialize with RudderStack
await Analytics.initialise({
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
})
// Track events
Analytics.trackEvent('ce_virtual_signup_form', {
action: 'signup_done',
signup_provider: 'email',
})
// Track page views
Analytics.pageView('/dashboard', 'Deriv App')
// Identify users
Analytics.identifyEvent('CR123456')
```
### Using Both RudderStack and PostHog
```typescript
import { Analytics } from '@deriv-com/analytics'
await Analytics.initialise({
// RudderStack for event tracking (required)
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
// PostHog for analytics and session recording (optional)
posthogOptions: {
apiKey: 'phc_YOUR_POSTHOG_KEY',
config: {
session_recording: {
recordCrossOriginIframes: true,
minimumDurationMilliseconds: 30000,
},
autocapture: true,
},
},
})
// Events are automatically sent to both providers
Analytics.trackEvent('ce_login_form', {
action: 'login_cta',
login_provider: 'google',
})
// User identification syncs with both providers
// When using PostHog, pass email via provider-specific traits (see User Identification section)
Analytics.identifyEvent('CR123456', {
rudderstack: { language: 'en', country_of_residence: 'US' },
posthog: { email: 'user@example.com', language: 'en', country_of_residence: 'US' },
})
```
## Framework Integration
### React Integration
The recommended pattern is a single `useAnalytics` hook that handles initialization and exposes all tracking methods:
```typescript
// hooks/useAnalytics.ts
import { useEffect } from 'react'
import { Analytics } from '@deriv-com/analytics'
let isInitialized = false
export function useAnalytics() {
useEffect(() => {
if (isInitialized) return
isInitialized = true
const rudderstackKey = process.env.REACT_APP_RUDDERSTACK_KEY // β replace with your env var
const posthogKey = process.env.REACT_APP_POSTHOG_KEY // β replace with your env var
if (!rudderstackKey && !posthogKey) return
Analytics.initialise({
...(rudderstackKey && { rudderstackKey }),
...(posthogKey && {
posthogOptions: {
apiKey: posthogKey,
api_host: process.env.REACT_APP_POSTHOG_HOST,
},
}),
debug: process.env.NODE_ENV === 'development',
})
}, [])
return {
trackEvent: Analytics.trackEvent,
identifyEvent: Analytics.identifyEvent,
pageView: Analytics.pageView,
loadEvent: Analytics.loadEvent,
setAttributes: Analytics.setAttributes,
reset: Analytics.reset,
}
}
```
Call the hook once at the top of your app:
```tsx
// App.tsx
import { useAnalytics } from './hooks/useAnalytics'
function App() {
const { trackEvent } = useAnalytics()
return <button onClick={() => trackEvent('ce_signup_button', { action: 'click' })}>Sign Up</button>
}
```
### Next.js Integration
#### App Router (Next.js 13+)
Use the same `useAnalytics` hook (with `NEXT_PUBLIC_` env var prefix) inside a dedicated client provider:
```typescript
// hooks/useAnalytics.ts
'use client'
import { useEffect } from 'react'
import { Analytics } from '@deriv-com/analytics'
let isInitialized = false
export function useAnalytics() {
useEffect(() => {
if (isInitialized) return
isInitialized = true
const rudderstackKey = process.env.NEXT_PUBLIC_RUDDERSTACK_KEY // β replace with your env var
const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY // β replace with your env var
if (!rudderstackKey && !posthogKey) return
Analytics.initialise({
...(rudderstackKey && { rudderstackKey }),
...(posthogKey && {
posthogOptions: {
apiKey: posthogKey,
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
},
}),
debug: process.env.NODE_ENV === 'development',
})
}, [])
return {
trackEvent: Analytics.trackEvent,
identifyEvent: Analytics.identifyEvent,
pageView: Analytics.pageView,
loadEvent: Analytics.loadEvent,
setAttributes: Analytics.setAttributes,
reset: Analytics.reset,
}
}
```
```tsx
// app/analytics-provider.tsx
'use client'
import { useAnalytics } from '@/hooks/useAnalytics'
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
useAnalytics()
return <>{children}</>
}
// app/layout.tsx
import { AnalyticsProvider } from './analytics-provider'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<AnalyticsProvider>{children}</AnalyticsProvider>
</body>
</html>
)
}
```
#### Pages Router (Next.js 12 and below)
```typescript
// pages/_app.tsx
import { useAnalytics } from '../hooks/useAnalytics'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
useAnalytics()
return <Component {...pageProps} />
}
```
### Vue.js Integration
```typescript
// main.ts or main.js
import { createApp } from 'vue'
import { Analytics } from '@deriv-com/analytics'
import App from './App.vue'
// Initialize analytics
Analytics.initialise({
rudderstackKey: import.meta.env.VITE_RUDDERSTACK_KEY,
posthogOptions: {
apiKey: import.meta.env.VITE_POSTHOG_KEY,
},
})
// Make Analytics available globally
const app = createApp(App)
app.config.globalProperties.$analytics = Analytics
app.mount('#app')
// Usage in components
export default {
methods: {
handleClick() {
this.$analytics.trackEvent('button_clicked', { button_name: 'submit' })
},
},
}
```
### Vanilla JavaScript
```html
<!doctype html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/@deriv-com/analytics@latest/dist/browser/analytics.bundle.global.js"></script>
</head>
<body>
<button id="signup-btn">Sign Up</button>
<script>
const { Analytics } = window.DerivAnalytics
// Initialize
Analytics.initialise({
rudderstackKey: 'YOUR_KEY',
posthogOptions: {
apiKey: 'YOUR_POSTHOG_KEY',
},
})
// Track button clicks
document.getElementById('signup-btn').addEventListener('click', () => {
Analytics.trackEvent('ce_signup_button', {
action: 'click',
location: 'header',
})
})
</script>
</body>
</html>
```
## Configuration
### RudderStack Configuration
RudderStack is used for event tracking and includes performance optimizations:
```typescript
await Analytics.initialise({
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
})
```
**Built-in Performance Features:**
- **Event Batching**: Flushes after 10 events or 10 seconds
- **SendBeacon API**: Uses `navigator.sendBeacon` for better performance on page unload
- **Automatic Retry**: Failed requests are automatically retried
- **Cookie Management**: Automatic anonymous ID generation and persistence (6-month cookie lifetime)
- **Offline Support**: Events are cached when offline and replayed when connection is restored
### PostHog Configuration
PostHog provides analytics, session recording, and feature flags.
#### Initialisation
`getPosthogInstance` (and `Analytics.initialise`) use a singleton β calling them more than once with the same key returns the existing instance without re-running the SDK init. Call once at app startup, not inside render loops.
```typescript
await Analytics.initialise({
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
posthogOptions: {
apiKey: 'phc_YOUR_KEY',
// Optional: override the auto-resolved API host (see table below)
api_host: 'https://ph.deriv.com',
// Optional: overridable settings (see "Overridable defaults" table below)
config: {
autocapture: false, // disable autocapture entirely
disable_session_recording: true, // opt out of session recording
session_recording: {
sessionRecordingSampleRate: 0.5, // record 50% of sessions
},
before_send: event => {
// your function runs after the built-in domain + timestamp filter
if (event?.properties?.sensitive_field) return null
return event
},
},
},
})
```
`api_host` is auto-resolved from `window.location.hostname` if omitted:
| Domain pattern | Resolved host |
| ---------------------- | ---------------------- |
| `*.deriv.me` | `https://ph.deriv.me` |
| `*.deriv.be` | `https://ph.deriv.be` |
| `*.deriv.ae` | `https://ph.deriv.ae` |
| all others (incl. SSR) | `https://ph.deriv.com` |
#### Enforced settings
These are applied **after** any consumer `config` spread. Passing them in `config` has no effect:
| Setting | Value | Reason |
| ----------------------------------------------- | ------------------- | ------------------------------------------------------ |
| `person_profiles` | `'identified_only'` | Prevents anonymous profile bloat |
| `capture_pageview` | `'history_change'` | SPA-safe β fires on every `pushState` / `replaceState` |
| `capture_pageleave` | `true` | Standard session completeness |
| `session_recording.recordCrossOriginIframes` | `true` | Captures embedded tools |
| `session_recording.minimumDurationMilliseconds` | `30000` | Filters sub-30-second noise sessions |
| `session_recording.maskAllInputs` | `true` | Privacy β cannot be lowered by consumers |
Consumer keys inside `session_recording` are spread **before** these enforced values, so extras like `sessionRecordingSampleRate` take effect without conflicting.
#### Overridable defaults
| Setting | Default | Override when⦠|
| ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
| `autocapture` | `{ dom_event_allowlist: ['click'] }` | You need more event types, or want to disable autocapture entirely |
| `rate_limiting.events_per_second` | `10` | Legitimate user flows are hitting the burst limiter |
| `rate_limiting.events_burst_limit` | `100` | Legitimate user flows are hitting the burst limiter |
#### β Do not capture `$pageview` manually
`capture_pageview: 'history_change'` is enforced and fires automatically on every client-side navigation. Adding a manual `posthog.capture('$pageview')` **doubles your pageview count** and contributes to `$client_ingestion_warning` rate-limit hits.
**React Router:**
```typescript
// β Remove this
useEffect(() => {
posthog.capture('$pageview')
}, [location.pathname])
// β
Nothing needed β capture_pageview: 'history_change' handles it
```
**Vue Router:**
```typescript
// β Remove this
router.afterEach(() => {
posthog.capture('$pageview')
})
// β
Nothing needed β capture_pageview: 'history_change' handles it
```
#### Domain allowlist
Events are silently blocked in `before_send` unless the hostname matches:
- `deriv.com`, `deriv.be`, `deriv.me`, `deriv.team`, `deriv.ae`
- `localhost` and `127.0.0.1` are always allowed
This list is hardcoded and not configurable.
#### Stale cookie cleanup
On every init, leftover `ph_*_posthog` cookies from previous or rotated API keys are removed automatically. No action needed.
## Core API
### Initialization
Initialize the analytics instance before tracking events:
```typescript
await Analytics.initialise({
rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
posthogOptions: {
apiKey: 'phc_YOUR_POSTHOG_KEY',
config: {
autocapture: true,
},
},
debug: false, // Enable to log all analytics calls to the console
})
```
### Event Tracking
Track custom events with any payload β there are no enforced property types. Send exactly what your event needs:
```typescript
Analytics.trackEvent('ce_login_form', {
action: 'login_cta',
login_provider: 'email',
form_name: 'main_login',
})
Analytics.trackEvent('ce_signup_form', {
action: 'signup_done',
signup_provider: 'google',
cta_information: {
cta_name: 'get_started',
section_name: 'hero',
},
})
```
### User Identification
Identify users and sync traits across analytics providers:
#### Simple Identification
```typescript
// Identify user with ID only
Analytics.identifyEvent('CR123456')
```
#### Identification with Custom Traits
```typescript
// Send same traits to both RudderStack and PostHog (no PostHog-specific fields)
Analytics.identifyEvent('CR123456', {
language: 'en',
country_of_residence: 'US',
account_type: 'real',
})
// Send provider-specific traits (recommended when using PostHog)
// PostHog requires `email` to automatically compute the `is_internal` flag
Analytics.identifyEvent('CR123456', {
rudderstack: {
language: 'en',
custom_field: 'value',
},
posthog: {
email: 'user@example.com', // Required for PostHog β used to set is_internal flag
language: 'en',
country_of_residence: 'US',
},
})
```
**How it works:**
- If you pass a simple object (e.g., `{ language: 'en' }`), the same traits are sent to both providers
- If you pass an object with `rudderstack` or `posthog` keys, provider-specific traits are used
- Queues identify calls if provider not yet initialized
- PostHog automatically handles aliasing between anonymous and identified users
- When `email` is provided in PostHog traits, the `is_internal` flag is automatically computed and set as a person property β `email` itself is **not** forwarded to PostHog
#### PostHog identity lifecycle
| Scenario | Call |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| User logs in | `identifyEvent(user_id, { posthog: { email, language, country_of_residence } })` |
| User logs out | `reset()` |
| User already identified in a previous session, person properties may be missing | `backfillPersonProperties({ user_id, email, language, country_of_residence })` |
**`identifyEvent`** links the anonymous PostHog session to the user and enforces `client_id`. Skip it if the current distinct ID is already the same `user_id` β the library does this check automatically.
**`reset`** clears the PostHog session on logout. Future events are anonymous until the next `identifyEvent`.
**`backfillPersonProperties`** fills in properties that may be missing on a returning user's profile (e.g. `client_id`, `is_internal`). It checks each property before writing and is a no-op if everything is already present. Call it once after the user ID is available, alongside or instead of `identifyEvent` for returning users.
> **Account-switch guard**: both `identifyEvent` and `backfillPersonProperties` detect when PostHog's stored distinct ID belongs to a _different_ identified user (not an anonymous UUID) and call `posthog.reset()` automatically before identifying the new user. This prevents profiles from merging across accounts.
### Page Views
Track page navigation:
```typescript
// Basic page view
Analytics.pageView('/dashboard')
// With custom platform name
Analytics.pageView('/dashboard', 'Deriv Trader')
// With additional properties
Analytics.pageView('/trade', 'Deriv App', {
section: 'multipliers',
instrument: 'forex',
})
```
**Note**: PostHog page views are captured automatically via the enforced `capture_pageview: 'history_change'` setting. Do not call `posthog.capture('$pageview')` manually β see the [β Do not capture `$pageview` manually](#-do-not-capture-pageview-manually) section. Manual page view tracking via `Analytics.pageView()` is primarily for RudderStack.
### User Attributes
Set user and context attributes that are automatically included in all subsequent events. Pass any key-value pairs β no fixed schema is enforced:
```typescript
Analytics.setAttributes({
country: 'US',
user_language: 'en',
account_type: 'real',
device_type: 'mobile',
account_currency: 'USD',
account_mode: 'demo',
residence_country: 'US',
loggedIn: true,
// any additional fields your app needs
})
```
### Reset User Session
Clear user session from all providers (e.g., on logout):
```typescript
Analytics.reset()
```
## Caching & Offline Support
The package includes automatic caching to ensure no events are lost β no extra configuration needed.
### localStorage Caching (SDK not yet loaded)
When you call `trackEvent` or `pageView` before `initialise()` completes, events are stored in `localStorage` and replayed automatically once the SDK loads:
```typescript
// Safe to call before initialise() β automatically replayed on load
Analytics.trackEvent('button_clicked', { button: 'submit' })
Analytics.pageView('/dashboard')
```
### In-Memory Caching (offline)
When the user is offline but the SDK is already initialized, events are held in memory and flushed on the next online `trackEvent` call:
```typescript
// While offline β queued in memory, sent automatically when back online
Analytics.trackEvent('offline_event', { data: 'cached' })
```
### Route-Based Events
Fire events only on specific pages using `loadEvent`:
```typescript
Analytics.loadEvent([
{
pages: ['dashboard', 'profile'],
event: { name: 'ce_page_load', properties: { page_type: 'authenticated' } },
},
{
excludedPages: ['login'],
event: { name: 'ce_authenticated_view', properties: {} },
},
])
```
## Debug Mode
Enable verbose logging to trace every analytics call in the browser console:
```typescript
await Analytics.initialise({
rudderstackKey: 'YOUR_KEY',
posthogOptions: { apiKey: 'phc_YOUR_KEY' },
debug: true,
})
```
All logs are prefixed with `[ANALYTIC]` (e.g., `[ANALYTIC][RudderStack] trackEvent | ...`). Useful during development and QA to verify events are firing correctly without opening the network tab.
## Advanced Usage
### Independent Package Usage
Each provider can be used independently for maximum flexibility:
#### PostHog Only
```typescript
import { Posthog } from '@deriv-com/analytics/posthog'
const posthog = Posthog.getPosthogInstance({
apiKey: 'phc_YOUR_KEY',
config: {
autocapture: true,
session_recording: {
recordCrossOriginIframes: true,
},
},
})
// Track events
posthog.capture('button_clicked', { button_name: 'signup' })
// Identify users β email is required and used to compute is_internal
posthog.identifyEvent('CR123', { email: 'user@example.com', language: 'en' })
// Check feature flags
const isEnabled = posthog.isFeatureEnabled('new-feature')
const variant = posthog.getFeatureFlag('button-color')
```
#### RudderStack Only
```typescript
import { RudderStack } from '@deriv-com/analytics/rudderstack'
const rudderstack = RudderStack.getRudderStackInstance('YOUR_KEY', () => {
console.log('RudderStack loaded')
})
// Track events
rudderstack.track('button_clicked', { button: 'signup' })
// Identify users
rudderstack.identifyEvent('CR123', { language: 'en' })
// Track page views
rudderstack.pageView('/dashboard', 'Deriv App', 'CR123')
```
### Access Provider Instances
Access raw provider instances for advanced use cases:
```typescript
const { tracking, posthog } = Analytics.getInstances()
// Access PostHog directly
if (posthog?.has_initialized) {
posthog.capture('custom_event', { property: 'value' })
// Access PostHog feature flags
const isEnabled = posthog.isFeatureEnabled('new-feature')
}
// Access RudderStack directly
if (tracking?.has_initialized) {
const userId = tracking.getUserId()
const anonId = tracking.getAnonymousId()
}
```
## PostHog Feature Flags
Access feature flags through the `posthog` instance:
```typescript
const { posthog } = Analytics.getInstances()
// Boolean flag β returns true, false, or undefined (not ready)
const isEnabled = posthog?.isFeatureEnabled('my-flag')
// Multivariate flag β returns a string variant, boolean, or undefined
const variant = posthog?.getFeatureFlag('button-color') // e.g. 'red' | 'blue' | true | undefined
// Structured payload attached to a flag
const config = posthog?.getFeatureFlagPayload('pricing-config') // e.g. { price: 9.99 }
// All active flags as a map
const allFlags = posthog?.getAllFlags() // { 'flag-a': true, 'flag-b': 'variant-x' }
// Subscribe to flag changes (fires immediately + on every reload)
const unsubscribe = posthog?.onFeatureFlags((flags, variants) => {
console.log('active flags:', flags)
console.log('variants:', variants)
})
// Call unsubscribe() to stop listening
// Force a reload from the server (e.g. after login or attribute change)
posthog?.reloadFeatureFlags()
```
When using PostHog directly (without the `Analytics` wrapper):
```typescript
import { Posthog } from '@deriv-com/analytics/posthog'
const posthog = Posthog.getPosthogInstance({ apiKey: 'phc_YOUR_KEY' })
const isEnabled = posthog.isFeatureEnabled('my-flag')
```
## PostHog Integration Checklist
Before shipping, verify:
- [ ] `Analytics.initialise` (or `getPosthogInstance`) is called **once** at app startup β not on every render or route change
- [ ] No `posthog.capture('$pageview')` calls remain β search the codebase and remove them
- [ ] `identifyEvent` is called on login with `email` in PostHog traits (needed for the `is_internal` flag)
- [ ] `reset()` is called on logout
- [ ] `backfillPersonProperties` is called for returning users when the user ID is available
- [ ] Your domain is in the allowlist β if testing on a non-`deriv.*` domain other than `localhost`, events are silently blocked
- [ ] `debug: true` is removed or guarded behind `process.env.NODE_ENV === 'development'`
## API Reference
### `initialise(options: Options): Promise<void>`
Initialize the analytics instance.
**Parameters:**
```typescript
interface Options {
rudderstackKey?: string
posthogOptions?: {
apiKey: string
/**
* Optional PostHog API host. If omitted, resolved automatically based on window.location.hostname:
* *.deriv.me β https://ph.deriv.me
* *.deriv.be β https://ph.deriv.be
* *.deriv.ae β https://ph.deriv.ae
* all others β https://ph.deriv.com (default; also used server-side)
*/
api_host?: string
config?: PostHogConfig
}
/** Enable verbose debug logging β all analytics calls are logged prefixed with [ANALYTIC] */
debug?: boolean
}
```
### `trackEvent(event: string, payload: Record<string, any>): void`
Track an event. No payload schema is enforced β send any key-value pairs.
### `pageView(url: string, platform?: string, properties?: Record<string, unknown>): void`
Track page navigation.
### `identifyEvent(userId?: string, traits?: Record<string, any>): void`
Link anonymous session to a user ID with optional traits. When PostHog is active and traits include an `email` field (via provider-specific `posthog` key), `is_internal` is automatically computed and set as a person property β the email itself is not stored in PostHog.
### `backfillPersonProperties({ user_id, email?, country_of_residence? }): void`
Backfills PostHog person properties for users identified in previous sessions. Sets `client_id` and `is_internal` if they are not already present. No-op if PostHog is not initialized or `user_id` is empty.
```typescript
// Call after PostHog has loaded and user ID is available
Analytics.backfillPersonProperties({ user_id: 'CR123456', email: 'user@example.com', country_of_residence: 'US' })
```
### `setAttributes(attributes: Record<string, any>): void`
Update user attributes that flow to all providers. No schema is enforced.
### `loadEvent(items: PageLoadEventConfig[]): void`
Fire events conditionally based on the current page pathname.
```typescript
type PageLoadEventConfig = {
pages?: string[] // fire only on these pages
excludedPages?: string[] // fire on all pages except these
event: { name: string; properties: Record<string, any> }
callback?: () => { name: string; properties: Record<string, any> }
}
```
### `reset(): void`
Clear user session from all providers.
### `getId(): string`
Get the current user ID.
### `getAnonymousId(): string`
Get the anonymous user ID.
### `getInstances(): { tracking, posthog }`
Access raw provider instances.
## Performance
### Benchmarks
- **Event tracking**: <5ms (average)
- **Page view tracking**: <3ms (average)
- **Initialization**: ~200ms (with both providers)
- **Offline cache replay**: <50ms for 10 events
### Optimizations
- **Tree-Shaking**: Unused providers completely removed from bundle
- **Lazy Loading**: PostHog loaded dynamically only when configured
- **Event Batching**: RudderStack batches events (10 events or 10 seconds)
- **SendBeacon**: Uses `navigator.sendBeacon` for reliable event delivery on page unload
- **Deduplication**: Prevents duplicate events from being sent
### Bundle Sizes
Estimated sizes (minified + gzipped):
- **Core (RudderStack + PostHog)**: ~32 KB
- **RudderStack Only**: ~18 KB
- **PostHog Only**: ~20 KB
- **Browser Bundle (all included)**: ~125 KB gzipped
## Troubleshooting
### Events not appearing in RudderStack
1. **Verify API key**: Check that `rudderstackKey` is correct
2. **Check network requests**: Open DevTools β Network tab β Look for requests to RudderStack dataplane
3. **Verify initialization**: Run `Analytics.getInstances().tracking.has_initialized` in console
4. **Check batching**: Events are batched - wait ~10 seconds or send 10 events
### PostHog not receiving events
1. **Verify API key**: Check that PostHog API key is correct (starts with `phc_`)
2. **Check domain allowlist**: Verify your domain is in the `allowedDomains` list
3. **Check initialization**: Run `Analytics.getInstances().posthog?.has_initialized` in console
4. **Verify network requests**: Check DevTools for requests to `ph.deriv.com` or your PostHog host
5. **Check browser console**: Look for PostHog errors or warnings
### Session recording not working
1. **Verify config**: Ensure `disable_session_recording: false` (or omit it)
2. **Check minimum duration**: Sessions shorter than `minimumDurationMilliseconds` are not saved
3. **Verify domain**: Check that PostHog is initialized and domain is allowed
4. **Check PostHog dashboard**: Recordings may take a few minutes to appear
### Events being cached but not sent
1. **Check online status**: Run `navigator.onLine` in console
2. **Verify SDK loaded**: Run `Analytics.getInstances().tracking.has_initialized`
3. **Check storage**: Open DevTools β Application β Local Storage β look for `cached_analytics_events` and `cached_analytics_page_views` keys. The `rudder_anonymous_id` is still stored as a cookie.
4. **Clear cache manually**: Clear localStorage keys or run `Analytics.reset()`
## Migration Guide
### From v1.x to v2.x
#### Breaking Changes
1. **identifyEvent signature changed**:
```typescript
// Old (v1.x) - hardcoded traits
Analytics.identifyEvent('CR123')
// New (v2.x) - custom traits
Analytics.identifyEvent('CR123', {
language: 'en',
country_of_residence: 'US',
})
// Or provider-specific
Analytics.identifyEvent('CR123', {
rudderstack: { language: 'en' },
posthog: { language: 'en', country_of_residence: 'US' },
})
```
2. **GrowthBook deprecated**: Migrate to PostHog feature flags
```typescript
// Old (GrowthBook)
const isEnabled = Analytics.isFeatureOn('new-feature')
// New (PostHog)
const { posthog } = Analytics.getInstances()
const isEnabled = posthog?.isFeatureEnabled('new-feature')
```
## Contributing
Contributions are welcome! Please follow these guidelines:
1. Fork the repository
2. Create a feature branch
3. Write tests for your changes
4. Run `npm test` and `npm run build`
5. Submit a pull request
## License
MIT
## Support
For issues and questions:
- **GitHub Issues**: https://github.com/binary-com/deriv-analytics/issues
- **Documentation**: https://github.com/binary-com/deriv-analytics