Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions __mocks__/@aptabase/react-native.ts

This file was deleted.

15 changes: 15 additions & 0 deletions __mocks__/countly-sdk-react-native-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Mock for Countly React Native SDK
* Used during testing to prevent actual analytics calls
*/

const mockCountly = {
init: jest.fn().mockResolvedValue(undefined),
start: jest.fn().mockResolvedValue(undefined),
enableCrashReporting: jest.fn().mockResolvedValue(undefined),
events: {
recordEvent: jest.fn().mockResolvedValue(undefined),
},
};

export default mockCountly;
207 changes: 207 additions & 0 deletions docs/countly-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# Analytics Migration: Aptabase to Countly

## Migration Completed

This project has been successfully migrated from Aptabase to Countly for analytics tracking while maintaining full backward compatibility.

## Previous Aptabase Implementation

Previously, the application used Aptabase for analytics with comprehensive error handling:

- **Aptabase Service**: Centralized error handling and retry logic
- **Aptabase Provider Wrapper**: Error-safe provider with fallback rendering
- **Simple Configuration**: Single app key configuration

## Current Countly Implementation

### 1. Countly Service (`src/services/analytics.service.ts`)

- **Purpose**: Centralized analytics tracking with error handling
- **Features**:
- Simple event tracking interface compatible with previous Aptabase interface
- Graceful error handling with retry logic
- Automatic service disable/enable after failures
- Comprehensive logging of events and errors
- Converts event properties to Countly segmentation format

### 2. Countly Provider Wrapper (`src/components/common/aptabase-provider.tsx`)

- **Purpose**: Initializes Countly SDK with error handling
- **Features**:
- Safe initialization with error recovery
- Uses the Countly service for error management
- Always renders children (no provider wrapper required)
- Configurable with app key and server URL
- Backward compatible `AptabaseProviderWrapper` export

Comment on lines +27 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect file references and init API in docs

Docs point to aptabase-provider.tsx and show Countly.init/start, but code uses countly-provider.tsx with CountlyConfig + initWithConfig. Correct to prevent developer confusion.

-### 2. Countly Provider Wrapper (`src/components/common/aptabase-provider.tsx`)
+### 2. Countly Provider (`src/components/common/countly-provider.tsx`)
@@
-- **Purpose**: Initializes Countly SDK with error handling
-  - Safe initialization with error recovery
-  - Uses the Countly service for error management
-  - Always renders children (no provider wrapper required)
-  - Configurable with app key and server URL
-  - Backward compatible `AptabaseProviderWrapper` export
+-- **Purpose**: Initializes Countly SDK with error handling
+  - Uses CountlyConfig + Countly.initWithConfig
+  - Safe initialization with error recovery via countlyService
+  - Always renders children (no provider context required)
+  - Configurable with app key and server URL
+  - Backward compatible `AptabaseProviderWrapper` export
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### 2. Countly Provider Wrapper (`src/components/common/aptabase-provider.tsx`)
- **Purpose**: Initializes Countly SDK with error handling
- **Features**:
- Safe initialization with error recovery
- Uses the Countly service for error management
- Always renders children (no provider wrapper required)
- Configurable with app key and server URL
- Backward compatible `AptabaseProviderWrapper` export
### 2. Countly Provider (`src/components/common/countly-provider.tsx`)
- **Purpose**: Initializes Countly SDK with error handling
- **Features**:
- Uses CountlyConfig + Countly.initWithConfig
- Safe initialization with error recovery via countlyService
- Always renders children (no provider context required)
- Configurable with app key and server URL
- Backward compatible `AptabaseProviderWrapper` export
🤖 Prompt for AI Agents
In docs/countly-migration.md around lines 27 to 36, update the incorrect file
and API references: replace references to "aptabase-provider.tsx" with
"countly-provider.tsx" and change the described initialization call from
Countly.init/start to the actual API used in code (use CountlyConfig and
initWithConfig). Also adjust the text about exports/compatibility to match the
real export names in countly-provider.tsx so the doc matches the implementation
and avoids developer confusion.

### 3. Updated Layout (`src/app/_layout.tsx`)

- **Purpose**: Uses the new Countly wrapper with updated configuration
- **Change**: Updated to pass both `COUNTLY_APP_KEY` and `COUNTLY_SERVER_URL`

## Key Benefits of Migration

### Enhanced Analytics Capabilities

- More detailed event segmentation
- Better crash reporting integration
- Real-time analytics dashboard
- Advanced user analytics features

### Improved Performance

- Lightweight SDK with optimized network usage
- Better error recovery mechanisms
- Enhanced offline support

### Better Configuration Control

- Separate app key and server URL configuration
- More granular control over analytics features
- Better integration with crash reporting

## Configuration

The system uses environment variables for Countly configuration:

- `COUNTLY_APP_KEY`: Countly application key
- `COUNTLY_SERVER_URL`: Countly server URL

When no app key is provided, the app runs without analytics entirely.

## Usage

The analytics interface remains exactly the same for backward compatibility:

### Using the Service Directly

```typescript
import { countlyService } from '@/services/analytics.service';

// Track a simple event
countlyService.trackEvent('user_login');

// Track an event with properties
countlyService.trackEvent('button_clicked', {
button_name: 'submit',
screen: 'login',
user_type: 'premium'
});
```

### Using the Hook (Recommended)

```typescript
import { useAnalytics } from '@/hooks/use-analytics';

const { trackEvent } = useAnalytics();

// Track events
trackEvent('screen_view', { screen_name: 'dashboard' });
trackEvent('feature_used', { feature_name: 'gps_tracking' });
```

## Testing

The implementation includes comprehensive unit tests:

### Countly Service Tests (`src/services/__tests__/countly.service.test.ts`)

- Event tracking functionality with Countly API
- Error handling logic
- Retry mechanism
- Disable/enable functionality
- Status tracking
- Timer-based recovery
- Property conversion to Countly segmentation format

### Countly Provider Tests (`src/components/common/__tests__/countly-provider.test.tsx`)

- Component rendering with Countly enabled/disabled
- Error handling integration
- Configuration validation
- Service integration
- Backward compatibility testing

### Analytics Hook Tests (`src/hooks/__tests__/use-analytics.test.ts`)

- Hook functionality
- Service integration
- Event tracking validation

All tests pass successfully and provide good coverage of the analytics functionality.

## Migration Notes

### Backward Compatibility

- All existing analytics calls work without changes
- `AptabaseProviderWrapper` is still exported and functional
- Service interface maintained identical to Aptabase version
- Environment variables changed from `APTABASE_*` to `COUNTLY_*`

### Technical Changes

- Replaced `@aptabase/react-native` with `countly-sdk-react-native-bridge`
- Updated service to convert properties to Countly segmentation format
- Enhanced provider initialization with crash reporting support
- Improved mock implementations for testing

Comment on lines +145 to +149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Align “Technical Changes” with actual initialization approach

Reflect modern initWithConfig usage rather than Countly.init/start in examples.

-- Enhanced provider initialization with crash reporting support
+- Enhanced provider initialization using CountlyConfig.initWithConfig with crash reporting enabled
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Replaced `@aptabase/react-native` with `countly-sdk-react-native-bridge`
- Updated service to convert properties to Countly segmentation format
- Enhanced provider initialization with crash reporting support
- Improved mock implementations for testing
- Replaced `@aptabase/react-native` with `countly-sdk-react-native-bridge`
- Updated service to convert properties to Countly segmentation format
- Enhanced provider initialization using CountlyConfig.initWithConfig with crash reporting enabled
- Improved mock implementations for testing
🤖 Prompt for AI Agents
In docs/countly-migration.md around lines 145 to 149, the "Technical Changes"
bullets currently mention provider initialization using Countly.init/start;
update the wording to reflect the modern initWithConfig usage instead: change
the initialization bullet to explicitly state migration to initWithConfig
(including crash reporting setup via the config), and adjust any example
references in that section to reference initWithConfig usage where appropriate
so the bullets align with the actual initialization approach shown elsewhere in
the document.

### No Breaking Changes

- All application functionality remains intact
- Analytics tracking continues to work seamlessly
- Error handling patterns preserved
- Performance characteristics maintained or improved

## Example Analytics Events

Here are some common analytics events with the new Countly implementation:

```typescript
// User authentication
countlyService.trackEvent('user_login', { method: 'email' });
countlyService.trackEvent('user_logout');

// Navigation
countlyService.trackEvent('screen_view', { screen_name: 'dashboard' });

// User actions
countlyService.trackEvent('button_clicked', {
button_name: 'emergency_call',
screen: 'home'
});

// Feature usage
countlyService.trackEvent('feature_used', {
feature_name: 'gps_tracking',
enabled: true
});

// Error tracking (in addition to Sentry)
countlyService.trackEvent('error_occurred', {
error_type: 'network',
component: 'api_client'
});
```

## Best Practices

1. **Keep event names consistent**: Use snake_case for event names
2. **Include relevant context**: Add properties that help understand user behavior
3. **Don't track sensitive data**: Avoid PII or sensitive information
4. **Use descriptive property names**: Make properties self-explanatory
5. **Track both success and failure**: Include error states for complete picture
6. **Leverage Countly's segmentation**: Use meaningful property values for better analytics

## Environment Setup

Add these variables to your environment configuration files (`.env.*`):

```bash
# Replace your existing Aptabase configuration
UNIT_COUNTLY_APP_KEY=your_countly_app_key_here
UNIT_COUNTLY_SERVER_URL=https://your-countly-server.com
```

The migration maintains full backward compatibility while providing enhanced analytics capabilities through Countly.
8 changes: 4 additions & 4 deletions env.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ const client = z.object({
UNIT_MAPBOX_DLKEY: z.string(),
IS_MOBILE_APP: z.boolean(),
SENTRY_DSN: z.string(),
APTABASE_URL: z.string(),
APTABASE_APP_KEY: z.string(),
COUNTLY_APP_KEY: z.string(),
COUNTLY_SERVER_URL: z.string(),
});

const buildTime = z.object({
Expand Down Expand Up @@ -125,8 +125,8 @@ const _clientEnv = {
UNIT_MAPBOX_PUBKEY: process.env.UNIT_MAPBOX_PUBKEY || '',
UNIT_MAPBOX_DLKEY: process.env.UNIT_MAPBOX_DLKEY || '',
SENTRY_DSN: process.env.UNIT_SENTRY_DSN || '',
APTABASE_APP_KEY: process.env.UNIT_APTABASE_APP_KEY || '',
APTABASE_URL: process.env.UNIT_APTABASE_URL || '',
COUNTLY_APP_KEY: process.env.UNIT_COUNTLY_APP_KEY || '',
COUNTLY_SERVER_URL: process.env.UNIT_COUNTLY_SERVER_URL || '',
};

/**
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"e2e-test": "maestro test .maestro/ -e APP_ID=com.obytes.development"
},
"dependencies": {
"@aptabase/react-native": "^0.3.10",
"@config-plugins/react-native-callkeep": "^11.0.0",
"@config-plugins/react-native-webrtc": "~12.0.0",
"@dev-plugins/react-query": "~0.2.0",
Expand Down Expand Up @@ -101,6 +100,7 @@
"axios": "~1.7.5",
"babel-plugin-module-resolver": "^5.0.2",
"buffer": "^6.0.3",
"countly-sdk-react-native-bridge": "^25.4.0",
"date-fns": "^4.1.0",
"expo": "^53.0.0",
"expo-application": "~6.1.5",
Expand Down
29 changes: 28 additions & 1 deletion src/app/(app)/__tests__/calls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ jest.mock('react-native', () => ({
),
RefreshControl: () => null,
View: ({ children, ...props }: any) => <div {...props}>{children}</div>,
StatusBar: {
setBackgroundColor: jest.fn(),
setTranslucent: jest.fn(),
setHidden: jest.fn(),
setBarStyle: jest.fn(),
},
}));

// Mock expo-router
Expand Down Expand Up @@ -170,12 +176,33 @@ jest.mock('lucide-react-native', () => ({
X: () => <div>✕</div>,
}));

// Mock useFocusEffect
// Mock navigation bar and color scheme
jest.mock('expo-navigation-bar', () => ({
setBackgroundColorAsync: jest.fn(() => Promise.resolve()),
setBehaviorAsync: jest.fn(() => Promise.resolve()),
setVisibilityAsync: jest.fn(() => Promise.resolve()),
}));

jest.mock('nativewind', () => ({
useColorScheme: jest.fn(() => ({ colorScheme: 'light' })),
}));

jest.mock('react-native-edge-to-edge', () => ({
SystemBars: ({ children, ...props }: any) => <div {...props}>{children}</div>,
}));

// Mock FocusAwareStatusBar
jest.mock('@/components/ui/focus-aware-status-bar', () => ({
FocusAwareStatusBar: () => null,
}));

// Mock useFocusEffect and useIsFocused
jest.mock('@react-navigation/native', () => ({
useFocusEffect: jest.fn((callback: () => void) => {
const React = require('react');
React.useEffect(callback, []);
}),
useIsFocused: jest.fn(() => true),
}));
Comment on lines 200 to 206
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Preserve other @react-navigation/native exports and wire cleanup correctly.

Exporting only two hooks can break consumers; also model cleanup via useEffect wrapper.

-jest.mock('@react-navigation/native', () => ({
-  useFocusEffect: jest.fn((callback: () => void) => {
-    const React = require('react');
-    React.useEffect(callback, []);
-  }),
-  useIsFocused: jest.fn(() => true),
-}));
+jest.mock('@react-navigation/native', () => {
+  const actual = jest.requireActual('@react-navigation/native');
+  const { useEffect } = require('react');
+  return {
+    ...actual,
+    useFocusEffect: jest.fn((callback: () => void | (() => void)) => {
+      useEffect(() => callback(), []);
+    }),
+    useIsFocused: jest.fn(() => true),
+  };
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
jest.mock('@react-navigation/native', () => ({
useFocusEffect: jest.fn((callback: () => void) => {
const React = require('react');
React.useEffect(callback, []);
}),
useIsFocused: jest.fn(() => true),
}));
jest.mock('@react-navigation/native', () => {
const actual = jest.requireActual('@react-navigation/native');
const { useEffect } = require('react');
return {
...actual,
useFocusEffect: jest.fn((callback: () => void | (() => void)) => {
useEffect(() => callback(), []);
}),
useIsFocused: jest.fn(() => true),
};
});
🤖 Prompt for AI Agents
In src/app/(app)/__tests__/calls.test.tsx around lines 200 to 206, the mock
replaces the entire @react-navigation/native module with only two hooks and
doesn't simulate effect cleanup; update the mock to import the real module via
jest.requireActual and spread its exports, then override only useFocusEffect and
useIsFocused; implement useFocusEffect as a jest.fn that uses React.useEffect to
call the provided callback and if that callback returns a cleanup function,
return that cleanup from the useEffect so teardown is modeled correctly, and
keep useIsFocused mocked to return true.


import CallsScreen from '../calls';
Expand Down
Loading
Loading