@deriv-com/analytics 1.41.1 → 1.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,10 @@ A modern, tree-shakeable analytics library for tracking user events with RudderS
28
28
  - [Configuration](#configuration)
29
29
  - [RudderStack](#rudderstack-configuration)
30
30
  - [PostHog](#posthog-configuration)
31
+ - [Enforced settings](#enforced-settings)
32
+ - [Overridable defaults](#overridable-defaults)
33
+ - [Do not capture $pageview manually](#-do-not-capture-pageview-manually)
34
+ - [Domain allowlist](#domain-allowlist)
31
35
  - [Core API](#core-api)
32
36
  - [Initialization](#initialization)
33
37
  - [Event Tracking](#event-tracking)
@@ -37,6 +41,8 @@ A modern, tree-shakeable analytics library for tracking user events with RudderS
37
41
  - [Caching & Offline Support](#caching--offline-support)
38
42
  - [Debug Mode](#debug-mode)
39
43
  - [Advanced Usage](#advanced-usage)
44
+ - [PostHog Feature Flags](#posthog-feature-flags)
45
+ - [PostHog Integration Checklist](#posthog-integration-checklist)
40
46
  - [API Reference](#api-reference)
41
47
  - [Performance](#performance)
42
48
  - [Troubleshooting](#troubleshooting)
@@ -379,52 +385,31 @@ await Analytics.initialise({
379
385
 
380
386
  ### PostHog Configuration
381
387
 
382
- PostHog provides powerful analytics, session recording, and feature flags:
388
+ PostHog provides analytics, session recording, and feature flags.
389
+
390
+ #### Initialisation
391
+
392
+ `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.
383
393
 
384
394
  ```typescript
385
395
  await Analytics.initialise({
386
396
  rudderstackKey: 'YOUR_RUDDERSTACK_KEY',
387
397
  posthogOptions: {
388
- // Required: API key
389
398
  apiKey: 'phc_YOUR_KEY',
390
399
 
391
- // Optional: Override the PostHog API host.
392
- // If omitted, the host is auto-selected at init time based on window.location.hostname:
393
- // *.deriv.me → https://ph.deriv.me
394
- // *.deriv.be → https://ph.deriv.be
395
- // *.deriv.ae → https://ph.deriv.ae
396
- // all others → https://ph.deriv.com (default, also used in SSR/non-browser environments)
397
- // Set this explicitly if you need to override the resolved host (e.g. in tests or custom deployments).
400
+ // Optional: override the auto-resolved API host (see table below)
398
401
  api_host: 'https://ph.deriv.com',
399
402
 
400
- // Optional: PostHog configuration
403
+ // Optional: overridable settings (see "Overridable defaults" table below)
401
404
  config: {
402
- // ui_host controls where the PostHog UI links point (e.g. session replay links).
403
- // This is separate from api_host and should remain pointed at the PostHog cloud UI.
404
- ui_host: 'https://us.posthog.com',
405
-
406
- // Session recording
405
+ autocapture: false, // disable autocapture entirely
406
+ disable_session_recording: true, // opt out of session recording
407
407
  session_recording: {
408
- recordCrossOriginIframes: true,
409
- maskAllInputs: false,
410
- minimumDurationMilliseconds: 30000, // Only save sessions longer than 30 seconds
408
+ sessionRecordingSampleRate: 0.5, // record 50% of sessions
411
409
  },
412
-
413
- // Feature capture
414
- autocapture: true, // Automatically capture clicks, form submissions, etc.
415
- capture_pageview: true, // Automatically capture page views
416
- capture_pageleave: true, // Capture when users leave pages
417
-
418
- // Console log recording (useful for debugging)
419
- enable_recording_console_log: true,
420
-
421
- // Disable features if needed
422
- disable_session_recording: false,
423
- disable_surveys: false,
424
-
425
- // Custom event filtering
426
410
  before_send: event => {
427
- // Custom logic to filter or modify events
411
+ // your function runs after the built-in domain + timestamp filter
412
+ if (event?.properties?.sensitive_field) return null
428
413
  return event
429
414
  },
430
415
  },
@@ -432,46 +417,77 @@ await Analytics.initialise({
432
417
  })
433
418
  ```
434
419
 
435
- #### Stale Cookie Cleanup
420
+ `api_host` is auto-resolved from `window.location.hostname` if omitted:
436
421
 
437
- On every PostHog initialization, the library automatically removes leftover `ph_*_posthog` cookies from previous or rotated API keys. This prevents stale cookies from accumulating in users' browsers when the PostHog project key changes.
422
+ | Domain pattern | Resolved host |
423
+ | ---------------------- | ---------------------- |
424
+ | `*.deriv.me` | `https://ph.deriv.me` |
425
+ | `*.deriv.be` | `https://ph.deriv.be` |
426
+ | `*.deriv.ae` | `https://ph.deriv.ae` |
427
+ | all others (incl. SSR) | `https://ph.deriv.com` |
438
428
 
439
- #### Domain Allowlisting
429
+ #### Enforced settings
440
430
 
441
- PostHog events are only sent from the following domains (hardcoded internally):
431
+ These are applied **after** any consumer `config` spread. Passing them in `config` has no effect:
442
432
 
443
- - `deriv.com`, `deriv.be`, `deriv.me`, `deriv.team`, `deriv.ae`
444
- - `localhost` and `127.0.0.1` are always allowed
433
+ | Setting | Value | Reason |
434
+ | ----------------------------------------------- | ------------------- | ------------------------------------------------------ |
435
+ | `person_profiles` | `'identified_only'` | Prevents anonymous profile bloat |
436
+ | `capture_pageview` | `'history_change'` | SPA-safe — fires on every `pushState` / `replaceState` |
437
+ | `capture_pageleave` | `true` | Standard session completeness |
438
+ | `session_recording.recordCrossOriginIframes` | `true` | Captures embedded tools |
439
+ | `session_recording.minimumDurationMilliseconds` | `30000` | Filters sub-30-second noise sessions |
440
+ | `session_recording.maskAllInputs` | `true` | Privacy — cannot be lowered by consumers |
441
+
442
+ Consumer keys inside `session_recording` are spread **before** these enforced values, so extras like `sessionRecordingSampleRate` take effect without conflicting.
443
+
444
+ #### Overridable defaults
445
445
 
446
- Events from any other domain are silently blocked. This list is not user-configurable.
446
+ | Setting | Default | Override when… |
447
+ | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------ |
448
+ | `autocapture` | `{ dom_event_allowlist: ['click'] }` | You need more event types, or want to disable autocapture entirely |
449
+ | `rate_limiting.events_per_second` | `10` | Legitimate user flows are hitting the burst limiter |
450
+ | `rate_limiting.events_burst_limit` | `100` | Legitimate user flows are hitting the burst limiter |
447
451
 
448
- #### Session Recording Customization
452
+ #### Do not capture `$pageview` manually
453
+
454
+ `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.
455
+
456
+ **React Router:**
449
457
 
450
458
  ```typescript
451
- posthogOptions: {
452
- apiKey: 'phc_YOUR_KEY',
453
- config: {
454
- session_recording: {
455
- // Record content from iframes
456
- recordCrossOriginIframes: true,
459
+ // ❌ Remove this
460
+ useEffect(() => {
461
+ posthog.capture('$pageview')
462
+ }, [location.pathname])
457
463
 
458
- // Mask sensitive input fields
459
- maskAllInputs: true,
460
- maskInputOptions: {
461
- password: true,
462
- email: true,
463
- },
464
+ // Nothing needed — capture_pageview: 'history_change' handles it
465
+ ```
464
466
 
465
- // Only save sessions longer than 1 minute
466
- minimumDurationMilliseconds: 60000,
467
+ **Vue Router:**
467
468
 
468
- // Sampling (record only 50% of sessions)
469
- sessionRecordingSampleRate: 0.5,
470
- },
471
- },
472
- }
469
+ ```typescript
470
+ // ❌ Remove this
471
+ router.afterEach(() => {
472
+ posthog.capture('$pageview')
473
+ })
474
+
475
+ // ✅ Nothing needed — capture_pageview: 'history_change' handles it
473
476
  ```
474
477
 
478
+ #### Domain allowlist
479
+
480
+ Events are silently blocked in `before_send` unless the hostname matches:
481
+
482
+ - `deriv.com`, `deriv.be`, `deriv.me`, `deriv.team`, `deriv.ae`
483
+ - `localhost` and `127.0.0.1` are always allowed
484
+
485
+ This list is hardcoded and not configurable.
486
+
487
+ #### Stale cookie cleanup
488
+
489
+ On every init, leftover `ph_*_posthog` cookies from previous or rotated API keys are removed automatically. No action needed.
490
+
475
491
  ## Core API
476
492
 
477
493
  ### Initialization
@@ -556,6 +572,22 @@ Analytics.identifyEvent('CR123456', {
556
572
  - PostHog automatically handles aliasing between anonymous and identified users
557
573
  - 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
558
574
 
575
+ #### PostHog identity lifecycle
576
+
577
+ | Scenario | Call |
578
+ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
579
+ | User logs in | `identifyEvent(user_id, { posthog: { email, language, country_of_residence } })` |
580
+ | User logs out | `reset()` |
581
+ | User already identified in a previous session, person properties may be missing | `backfillPersonProperties({ user_id, email, language, country_of_residence })` |
582
+
583
+ **`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.
584
+
585
+ **`reset`** clears the PostHog session on logout. Future events are anonymous until the next `identifyEvent`.
586
+
587
+ **`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.
588
+
589
+ > **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.
590
+
559
591
  ### Page Views
560
592
 
561
593
  Track page navigation:
@@ -574,7 +606,7 @@ Analytics.pageView('/trade', 'Deriv App', {
574
606
  })
575
607
  ```
576
608
 
577
- **Note**: PostHog automatically captures page views when `capture_pageview: true` is set in config. Manual page view tracking is primarily for RudderStack.
609
+ **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.
578
610
 
579
611
  ### User Attributes
580
612
 
@@ -729,6 +761,57 @@ if (tracking?.has_initialized) {
729
761
  }
730
762
  ```
731
763
 
764
+ ## PostHog Feature Flags
765
+
766
+ Access feature flags through the `posthog` instance:
767
+
768
+ ```typescript
769
+ const { posthog } = Analytics.getInstances()
770
+
771
+ // Boolean flag — returns true, false, or undefined (not ready)
772
+ const isEnabled = posthog?.isFeatureEnabled('my-flag')
773
+
774
+ // Multivariate flag — returns a string variant, boolean, or undefined
775
+ const variant = posthog?.getFeatureFlag('button-color') // e.g. 'red' | 'blue' | true | undefined
776
+
777
+ // Structured payload attached to a flag
778
+ const config = posthog?.getFeatureFlagPayload('pricing-config') // e.g. { price: 9.99 }
779
+
780
+ // All active flags as a map
781
+ const allFlags = posthog?.getAllFlags() // { 'flag-a': true, 'flag-b': 'variant-x' }
782
+
783
+ // Subscribe to flag changes (fires immediately + on every reload)
784
+ const unsubscribe = posthog?.onFeatureFlags((flags, variants) => {
785
+ console.log('active flags:', flags)
786
+ console.log('variants:', variants)
787
+ })
788
+ // Call unsubscribe() to stop listening
789
+
790
+ // Force a reload from the server (e.g. after login or attribute change)
791
+ posthog?.reloadFeatureFlags()
792
+ ```
793
+
794
+ When using PostHog directly (without the `Analytics` wrapper):
795
+
796
+ ```typescript
797
+ import { Posthog } from '@deriv-com/analytics/posthog'
798
+
799
+ const posthog = Posthog.getPosthogInstance({ apiKey: 'phc_YOUR_KEY' })
800
+ const isEnabled = posthog.isFeatureEnabled('my-flag')
801
+ ```
802
+
803
+ ## PostHog Integration Checklist
804
+
805
+ Before shipping, verify:
806
+
807
+ - [ ] `Analytics.initialise` (or `getPosthogInstance`) is called **once** at app startup — not on every render or route change
808
+ - [ ] No `posthog.capture('$pageview')` calls remain — search the codebase and remove them
809
+ - [ ] `identifyEvent` is called on login with `email` in PostHog traits (needed for the `is_internal` flag)
810
+ - [ ] `reset()` is called on logout
811
+ - [ ] `backfillPersonProperties` is called for returning users when the user ID is available
812
+ - [ ] Your domain is in the allowlist — if testing on a non-`deriv.*` domain other than `localhost`, events are silently blocked
813
+ - [ ] `debug: true` is removed or guarded behind `process.env.NODE_ENV === 'development'`
814
+
732
815
  ## API Reference
733
816
 
734
817
  ### `initialise(options: Options): Promise<void>`