> ## Documentation Index
> Fetch the complete documentation index at: https://cona.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Analytics posthog

# Analytics (PostHog)

CONA uses [PostHog](https://posthog.com/) for product analytics and user behavior tracking. This guide explains how our PostHog integration works, including user identification, event tracking, and best practices.

## Overview

PostHog provides both client-side and server-side analytics tracking, allowing us to understand user behavior across the entire application. Our implementation is designed around CONA's multi-tenant architecture using the actor/user system.

## Integration Architecture

### Client-Side Integration

The PostHog client is initialized in the `PostHogProvider` component:

```tsx theme={null}
// apps/webapp/app/providers/post-hog-provider.tsx
export function PostHogProvider({ children }: { children: React.ReactNode }) {
  const { user, isLoading: isUserLoading } = useUser();
  const { actorId, organizationId, organizationName, isLoading: isAuthLoading } = useAuth();

  useEffect(() => {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
      capture_pageview: false, // Manual pageview capture
      capture_pageleave: true,
    });
  }, []);
  
  // User identification logic...
}
```

### Server-Side Integration

Server-side tracking uses the PostHog Node.js client:

```typescript theme={null}
// apps/webapp/app/lib/services/posthog.ts
import { PostHog } from "posthog-node";

export default function PostHogClient() {
  const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    host: process.env.NEXT_PUBLIC_POSTHOG_HOST!,
    flushAt: 1,
    flushInterval: 0,
  });
  return posthogClient;
}
```

**Note**: The server-side PostHog client has different API signatures than the client-side version:

```typescript theme={null}
// Client-side API
posthog.identify(userId, { email: "..." });

// Server-side API  
posthog.identify({
  distinctId: userId,
  properties: { email: "..." }
});
```

## Helper Functions

### PostHog Tracking Helper

We provide a reusable helper function for consistent event tracking:

```typescript theme={null}
// apps/webapp/app/lib/utils/posthog-tracking.ts
import { trackEventWithAuth } from "@/app/lib/utils/posthog-tracking";

// Simple usage with auth context
await trackEventWithAuth(
  userId,
  organizationId,
  organizationName,
  "event_name",
  {
    custom_property: "value",
    // ... other properties
  }
);

// Advanced usage with options
await trackEvent({
  userId,
  eventName: "event_name",
  properties: { custom_property: "value" },
  organizationId,
  organizationName,
  skipIdentification: false, // Set to true if user was recently identified
});
```

**Benefits of using the helper:**

* Automatic user identification with proper fallback logic
* Consistent organization context
* Error handling that doesn't break main operations
* TypeScript support for better development experience

## User Identification System

### Migration from actorId to userId (Feb 2026)

**Important**: We previously used `actorId` (organization-scoped) as the distinct ID, which created duplicate users in PostHog when the same person belonged to multiple organizations. We have now migrated to using `userId` (global, stable identifier) as the distinct ID.

**What changed:**

* ✅ Distinct ID changed from `actorId` to `userId`
* ✅ Added PostHog groups for organization-level tracking
* ✅ Created migration script to alias existing duplicates
* ✅ All tracking code updated to use `userId`

**If you need to run the migration:**

```bash theme={null}
# Run with your environment
pnpm dlx dotenv-cli -e .env.local -- node scripts/posthog-alias-duplicate-users.mjs
```

### Why We Use User IDs

CONA uses a multi-tenant architecture where:

* **Users** represent the actual person/account (global identity)
* **Actors** represent the user's identity within a specific organization

For PostHog analytics, we use the **userId** as the distinct ID, not the actorId. This ensures:

* ✅ The same person is tracked consistently across all organizations
* ✅ No duplicate user entries when switching organizations
* ✅ Accurate cross-organization user journey tracking
* ✅ Organization-level analytics via PostHog groups

### Identification Flow

1. **Authentication**: User logs in via Auth0
2. **Context Loading**: App loads user context (`userId`, `organizationId`, `organizationName`)
3. **PostHog Identification**: When both user and organization context are available, PostHog:
   * Identifies the user with `userId` as distinct ID
   * Associates the user with their organization via groups
4. **Event Tracking**: All events are tracked using the same `userId` and organization group

```typescript theme={null}
// Client-side identification
posthog.identify(userId, {
  email: user.email,
  name: user.name || user.email,
});

// Set organization as a group
posthog.group("organization", organizationId, {
  name: organizationName,
  slug: organizationSlug,
});
```

### User Properties

Each identified user has the following properties:

| Property        | Source             | Description                                       |
| --------------- | ------------------ | ------------------------------------------------- |
| **Distinct ID** | `userId`           | Unique identifier for the person (global, stable) |
| **Email**       | Auth0 user profile | User's email address                              |
| **Name**        | Auth0 user profile | User's display name                               |

### Organization Tracking via Groups

Organization-level data is tracked using PostHog groups:

| Property       | Type               | Description                            |
| -------------- | ------------------ | -------------------------------------- |
| **Group Type** | `organization`     | Fixed group type for all organizations |
| **Group Key**  | `organizationId`   | Unique identifier for the organization |
| **Group Name** | `organizationName` | Display name for the organization      |
| **Group Slug** | `organizationSlug` | URL-friendly organization identifier   |

This allows you to:

* Filter events by organization
* Track organization-level metrics
* Understand which organizations are most active
* Analyze feature adoption per organization
* Query by organization slug for URL-based filtering

## Currently Tracked Events

### Organization Management

* ✅ `organization_onboarding_completed` - When organization setup is complete

### App Store & Installation

* ✅ `app_installed` - When an app is installed from the app store (one-time event)

### Integration Management (Business Logic)

* ✅ `integration_created` - When a new integration is created and configured
* ✅ `integration_activated` - When an integration is turned on (the important toggle)
* ✅ `integration_deactivated` - When an integration is turned off (the important toggle)
* ✅ `integration_deleted` - When an integration is completely removed

### Event Separation Logic

We separate **App Store events** (installation) from **Integration events** (business usage):

* **App Installation**: One-time event when installing from app store
* **Integration Usage**: Ongoing business events when users actually use/configure the integration

This prevents double-tracking and focuses on the events that matter for business analytics.

### Automatic Events

* **Pageviews**: Manually captured with full URL including search params
* **Page Leave**: Automatically captured when users navigate away

## Event Tracking Examples

### Custom Events

Server actions can track custom events using the helper function:

```typescript theme={null}
// Example from app installation
await trackEventWithAuth(
  userId,
  organizationId,
  organizationName,
  "app_installed",
  {
    app_id,
    app_name: installation.app?.name || "Unknown App",
    app_slug: installation.app?.slug || "unknown",
    app_type: installation.app?.app_type || "UNKNOWN",
    initial_status: status,
    has_config: !!config,
    config_keys: config ? Object.keys(config) : [],
  }
);
```

### Event Properties

Always include these properties for consistency:

* `organization_id`: For organization-specific analysis
* `organization_name`: Human-readable organization identifier
* Context-specific data relevant to the event

## Best Practices

### 1. Use the Helper Function

Always use `trackEventWithAuth` or `trackEvent` instead of calling PostHog directly:

```typescript theme={null}
// ✅ Correct - uses helper with consistent identification
await trackEventWithAuth(userId, organizationId, organizationName, "event_name", properties);

// ❌ Incorrect - manual PostHog calls lack consistency
const posthog = PostHogClient();
posthog.capture({ distinctId: userId, event: "event_name" });
```

### 2. Include Organization Context

Always include organization information in events:

```typescript theme={null}
// ✅ Good - includes organization context via helper
await trackEventWithAuth(
  userId,
  organizationId,
  organizationName,
  "document_created",
  {
    document_type: "invoice",
    // ... other properties
  }
);
```

### 3. Event Naming Convention

Use consistent event naming:

* Use snake\_case for event names
* Include the action and object: `document_created`, `user_invited`, `payment_processed`
* Be specific but not overly verbose

### 4. Property Naming

* Use snake\_case for property names
* Include units in property names: `amount_cents`, `duration_seconds`
* Use consistent property names across similar events

### 5. Error Handling

The helper functions include error handling that won't break your main operations:

```typescript theme={null}
// Tracking errors are logged but don't throw exceptions
await trackEventWithAuth(userId, organizationId, organizationName, "event_name", properties);
// Main operation continues even if tracking fails
```

## Privacy and Compliance

### Data Collection

* Only collect necessary data for product improvement
* Respect user privacy settings
* Follow GDPR and other applicable regulations

### Personal Data

* Email addresses are collected for user identification
* No sensitive business data should be sent to PostHog
* Use hashed or anonymized identifiers when possible

## Troubleshooting

### Common Issues

1. **Events not appearing**: Check that `userId` is available before sending events
2. **Duplicate users**: Ensure consistent use of `userId` across client and server (this was fixed in the migration from actorId)
3. **Missing organization context**: Verify that `organizationId` is included in event properties
4. **Events without user information**: This happens when server-side events are fired before client-side identification. Always call `posthog.identify()` in server actions before `posthog.capture()`

### Timing Issues

**Problem**: Server-side events fired during onboarding or user creation might not have proper user identification.

**Solution**: Always identify users server-side before capturing events:

```typescript theme={null}
// ❌ Problem - event might be orphaned
posthog.capture({
  distinctId: userId,
  event: "organization_onboarding_completed",
  // ... properties
});

// ✅ Solution - use helper function which handles identification
await trackEventWithAuth(userId, organizationId, organizationName, "organization_onboarding_completed", properties);
```

## Adding New Event Tracking

### 1. Use the Helper Function

```typescript theme={null}
import { trackEventWithAuth } from "@/app/lib/utils/posthog-tracking";

// In your server action
export async function yourAction() {
  try {
    const { organizationId, organizationName, userId } = await requireAuth();
    
    // Your business logic here
    const result = await someBusinessOperation();
    
    // Track the event
    await trackEventWithAuth(
      userId,
      organizationId,
      organizationName,
      "your_event_name",
      {
        // Event-specific properties
        property_name: "value",
        has_something: !!result.something,
        count: result.items.length,
      }
    );
    
    return { success: true, data: result };
  } catch (error) {
    // Handle error...
  }
}
```

### 2. Event Naming Guidelines

* Use descriptive, consistent names
* Follow the pattern: `{object}_{action}` (e.g., `document_created`, `integration_activated`)
* Use snake\_case for all event names and properties

### 3. Property Guidelines

* Include relevant context (IDs, names, counts, boolean flags)
* Use consistent property naming across similar events
* Include organization context (handled automatically by helper)
* Add timestamps when relevant (handled automatically by helper)
