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

# Analytics, Push & Feedback

> Track engagement, send targeted push, collect love score, and close the feedback loop

# Use Case: Analytics, Push & Feedback

This guide covers the retention and measurement side of your app: tracking meaningful events, registering for push, measuring user sentiment with surveys, prompting for App Store reviews, and closing the feedback loop.

***

## What You'll Build

```
Track key events → Register for push → Handle push taps →
Measure love score (NPS/CSAT) → Smart review prompting → React to feedback
```

***

## Step 1: Track Meaningful Events

Custom events power everything in AppDNA -- analytics, experiment goals, survey triggers, in-app message triggers, and webhook payloads.

### What to Track

Focus on events that represent **meaningful user actions**:

**iOS**

```swift theme={null}
// Key user actions
AppDNA.track(event: "workout_completed", properties: [
    "duration_minutes": 45,
    "type": "strength",
    "calories": 320
])

// Engagement milestones
AppDNA.track(event: "streak_achieved", properties: [
    "streak_days": 7
])

// Feature usage
AppDNA.track(event: "recipe_saved", properties: [
    "recipe_id": "r-456",
    "category": "dinner"
])

// Goal changes
AppDNA.track(event: "goal_updated", properties: [
    "from": "lose_weight",
    "to": "build_muscle"
])
```

**Android**

```kotlin theme={null}
AppDNA.track(
    event = "workout_completed",
    properties = mapOf(
        "duration_minutes" to 45,
        "type" to "strength",
        "calories" to 320
    )
)

AppDNA.track(event = "streak_achieved", properties = mapOf("streak_days" to 7))

AppDNA.track(event = "recipe_saved", properties = mapOf(
    "recipe_id" to "r-456", "category" to "dinner"
))
```

**Flutter**

```dart theme={null}
await AppDNA.track("workout_completed", properties: {
  "duration_minutes": 45,
  "type": "strength",
  "calories": 320,
});

await AppDNA.track("streak_achieved", properties: {"streak_days": 7});

await AppDNA.track("recipe_saved", properties: {
  "recipe_id": "r-456",
  "category": "dinner",
});
```

**React Native**

```typescript theme={null}
await AppDNA.track("workout_completed", {
  duration_minutes: 45,
  type: "strength",
  calories: 320,
});

await AppDNA.track("streak_achieved", { streak_days: 7 });

await AppDNA.track("recipe_saved", {
  recipe_id: "r-456",
  category: "dinner",
});
```

### Event Design Guidelines

* Use `snake_case` for event names and property keys
* Include relevant properties (duration, type, count, category)
* **Don't** duplicate [auto-tracked events](https://docs.appdna.ai/api-reference/auto-tracked-events.md) -- the SDK already tracks session, onboarding, paywall, billing, push, and experiment events
* Events automatically include session ID, device info, and active experiment exposures

> [!NOTE]
>
> Custom events can be used as experiment conversion goals, webhook triggers, in-app message triggers, and survey triggers -- all configured in the Console with no code changes.

***

## Step 2: Set Up Push Notifications

### Register for Push

**iOS**

```swift theme={null}
// Request permission and register token
let granted = await AppDNA.registerForPush()

// Set delegate for push callbacks
AppDNA.pushModule.setDelegate(self)

// If you handle token registration yourself:
func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    AppDNA.setPushToken(token)
}
```

**Android**

```kotlin theme={null}
// Request permission (Android 13+)
AppDNA.push.requestPermission(activity)

// Register FCM token
class MyFirebaseService : FirebaseMessagingService() {
    override fun onNewToken(token: String) {
        AppDNA.setPushToken(token)
    }
}

// Set delegate
AppDNA.push.setDelegate(this)
```

**Flutter**

```dart theme={null}
final granted = await AppDNA.push.requestPermission();
AppDNA.push.setDelegate(MyPushHandler());
```

**React Native**

```typescript theme={null}
const granted = await AppDNA.push.requestPermission();
```

### Track Push Engagement

AppDNA pushes are tracked for you: the SDK records `push_delivered` and `push_tapped` (with the delivery id) for every
push it handles — on iOS through its automatic notification handler, on Android through its messaging service and
`AppDNA.handlePushTap(intent)`, or your forwarding with `AppDNA.push.handleMessage` / `handleTapData`. Do not also track those pushes by hand, or they are counted
twice. The manual calls below are **only for pushes the SDK does not see** (for example a push shown by another system),
and they run in your app, not in a Notification Service Extension:

**iOS**

```swift theme={null}
// Only for a push the SDK does not see (delivery)
AppDNA.trackPushDelivered(pushId: notification.pushId)

// In tap handler
AppDNA.trackPushTapped(pushId: notification.pushId, action: "open_workout")
```

**Android**

```kotlin theme={null}
AppDNA.trackPushDelivered(pushId = notification.pushId)
AppDNA.trackPushTapped(pushId = notification.pushId, action = "open_workout")
```

**Flutter**

```dart theme={null}
AppDNA.push.trackDelivered(notification.pushId);
AppDNA.push.trackTapped(notification.pushId, action: "open_workout");
```

**React Native**

```typescript theme={null}
AppDNA.push.trackDelivered(notification.push_id);
AppDNA.push.trackTapped(notification.push_id, "open_workout");
```

### Handle Push Tap Callbacks

Route the user to the right screen when they tap a push notification:

**iOS**

```swift theme={null}
class PushHandler: AppDNAPushDelegate {
    func onPushTapped(notification: PushPayload, actionId: String?) {
        guard let action = notification.action else { return }

        switch action.type {
        case "deep_link":
            navigate(to: action.value)
        case "screen":
            openScreen(action.value)
        default:
            break
        }
    }

    func onPushReceived(notification: PushPayload, inForeground: Bool) {
        if inForeground {
            showInAppBanner(title: notification.title, body: notification.body)
        }
    }
}
```

**Android**

```kotlin theme={null}
override fun onPushTapped(notification: PushPayload, actionId: String?) {
    notification.action?.let { action ->
        when (action.type) {
            "deep_link" -> navigate(action.value)
            "screen" -> openScreen(action.value)
        }
    }
}
```

**Flutter**

```dart theme={null}
@override
void onPushTapped(Map<String, dynamic> notification, String? actionId) {
  final value = notification["action_value"] as String?;
  if (value != null) {
    navigate(value);
  }
}
```

**React Native**

```typescript theme={null}
AppDNA.push.setDelegate({
  onPushTokenRegistered: (token) => {},
  onPushReceived: (notification, inForeground) => {},
  onPushTapped: (notification, actionId) => {
    if (notification.action_type === "deep_link") {
      navigate(notification.action_value as string);
    }
  },
});
```

***

## Step 3: Measure Love Score with Surveys

### Trigger-Based (Recommended)

Configure surveys in the Console to appear automatically based on events and conditions. For example:

* **NPS survey** after 7 days of usage and 5+ sessions
* **CSAT survey** after the user completes their 10th workout
* **Feedback prompt** after a support interaction

No code needed for trigger-based surveys -- they appear automatically.

### Manual Trigger

For precise control over timing, present a survey manually:

**iOS**

```swift theme={null}
// After a key moment (e.g., user completed their first goal)
AppDNA.surveys.present("nps_q1_2026")
```

**Android**

```kotlin theme={null}
AppDNA.surveys.present("nps_q1_2026")
```

**Flutter**

```dart theme={null}
await AppDNA.surveys.present("nps_q1_2026");
```

**React Native**

```typescript theme={null}
await AppDNA.surveys.present("nps_q1_2026");
```

### Capture Survey Responses

Use the survey delegate to react to responses in real-time:

**iOS**

```swift theme={null}
class FeedbackHandler: AppDNASurveyDelegate {
    func onSurveyCompleted(surveyId: String, responses: [SurveyResponse]) {
        // Each response is a SurveyResponse with a questionId + answer.
        // Look answers up by the question IDs configured in the Console.
        for response in responses {
            switch response.questionId {
            case "nps_question":
                if let score = response.answer as? Int {
                    handleNPSScore(score)
                }
            case "free_text_question":
                if let feedback = response.answer as? String {
                    logFeedback(feedback)
                }
            default:
                break
            }
        }
    }

    private func handleNPSScore(_ score: Int) {
        if score >= 9 {
            // Promoter -- show referral prompt
            showReferralPrompt()
        } else if score <= 6 {
            // Detractor -- show support link
            showSupportLink()
        }
        // Passives (7-8) -- no action
    }
}

AppDNA.surveys.setDelegate(FeedbackHandler())
```

**Android**

```kotlin theme={null}
AppDNA.surveys.setDelegate(object : AppDNASurveyDelegate {
    override fun onSurveyCompleted(surveyId: String, responses: List<SurveyResponse>) {
        val score = responses.firstOrNull { it.questionId == "nps_question" }?.answer as? Int ?: return

        when {
            score >= 9 -> showReferralPrompt()
            score <= 6 -> showSupportLink()
        }
    }
})
```

**Flutter**

```dart theme={null}
class FeedbackHandler extends AppDNASurveyDelegate {
  @override
  void onSurveyCompleted(String surveyId, List<Map<String, dynamic>> responses) {
    // Each entry is an answer map: {'questionId': String, 'answer': dynamic}.
    final nps = responses.firstWhere(
      (r) => r["questionId"] == "nps_question",
      orElse: () => {},
    );
    final score = (nps["answer"] as int?) ?? 0;

    if (score >= 9) showReferralPrompt();
    else if (score <= 6) showSupportLink();
  }
}

AppDNA.surveys.setDelegate(FeedbackHandler());
```

**React Native**

```typescript theme={null}
AppDNA.surveys.setDelegate({
  onSurveyPresented: (surveyId) => {},
  onSurveyDismissed: (surveyId) => {},
  onSurveyCompleted: (surveyId, responses) => {
    // `responses` is a list of { questionId, answer } maps.
    const nps = responses.find((r) => r.questionId === "nps_question");
    const score = (nps?.answer as number) ?? 0;

    if (score >= 9) showReferralPrompt();
    else if (score <= 6) showSupportLink();
  },
});
```

***

## Step 4: Smart Review Prompting

The smart review prompt is a two-step flow that maximizes positive App Store / Play Store reviews:

1. **In-app question:** "Do you enjoy using \[app]?"
2. **Positive response** -- shows the native store review dialog
3. **Negative response** -- shows a free-text feedback form (captures the complaint instead of sending them to the store)

The review prompt is configured in the Console as a survey and delivered by the SDK's survey pipeline -- there is no dedicated `promptReview` method. Either let it fire automatically from a Console trigger (recommended), or present the review survey manually after a positive moment:

**iOS**

```swift theme={null}
// Trigger after a positive moment (e.g., streak achieved)
AppDNA.surveys.present("smart_review")
```

**Android**

```kotlin theme={null}
AppDNA.surveys.present("smart_review")
```

**Flutter**

```dart theme={null}
await AppDNA.surveys.present("smart_review");
```

**React Native**

```typescript theme={null}
await AppDNA.surveys.present("smart_review");
```

> [!WARNING]
>
> Rate-limited: the native store review dialog shows at most 3 times per year, at least 90 days apart. The SDK enforces these limits automatically -- even if the review survey is presented, the SDK decides whether to actually show the store dialog.

***

## Step 5: Close the Feedback Loop

### React to Feedback in Your App

Use survey callbacks to create follow-up experiences based on sentiment:

**iOS**

```swift theme={null}
func onSurveyCompleted(surveyId: String, responses: [SurveyResponse]) {
    guard let nps = responses.first(where: { $0.questionId == "nps_question" }),
          let score = nps.answer as? Int else { return }

    if score >= 9 {
        // Promoter: encourage sharing
        showReferralPrompt()
        AppDNA.track(event: "nps_promoter", properties: ["score": score])
    } else if score <= 6 {
        // Detractor: offer help
        showSupportOptions()
        AppDNA.track(event: "nps_detractor", properties: ["score": score])
    }
}
```

**Android**

```kotlin theme={null}
override fun onSurveyCompleted(surveyId: String, responses: List<SurveyResponse>) {
    val score = responses.firstOrNull { it.questionId == "nps_question" }?.answer as? Int ?: return

    if (score >= 9) {
        showReferralPrompt()
        AppDNA.track(event = "nps_promoter", properties = mapOf("score" to score))
    } else if (score <= 6) {
        showSupportOptions()
        AppDNA.track(event = "nps_detractor", properties = mapOf("score" to score))
    }
}
```

**Flutter**

```dart theme={null}
@override
void onSurveyCompleted(String surveyId, List<Map<String, dynamic>> responses) {
  final nps = responses.firstWhere(
    (r) => r["questionId"] == "nps_question",
    orElse: () => {},
  );
  final score = (nps["answer"] as int?) ?? 0;

  if (score >= 9) {
    showReferralPrompt();
    AppDNA.track("nps_promoter", properties: {"score": score});
  } else if (score <= 6) {
    showSupportOptions();
    AppDNA.track("nps_detractor", properties: {"score": score});
  }
}
```

**React Native**

```typescript theme={null}
AppDNA.surveys.setDelegate({
  onSurveyPresented: (surveyId) => {},
  onSurveyDismissed: (surveyId) => {},
  onSurveyCompleted: (surveyId, responses) => {
    const score = (responses.find((r) => r.questionId === "nps_question")?.answer as number) ?? 0;

    if (score >= 9) {
      showReferralPrompt();
      AppDNA.track("nps_promoter", { score });
    } else if (score <= 6) {
      showSupportOptions();
      AppDNA.track("nps_detractor", { score });
    }
  },
});
```

### Connect Events to Push Campaigns

In the Console, you can target push campaigns based on user behavior and survey responses:

* Send "We've improved based on your feedback" push to users who gave `NPS <= 6`
* Send "Share with friends" push to users who gave `NPS >= 9`
* Send re-engagement pushes to users who haven't triggered `workout_completed` in 7 days
* Send milestone congratulations for `streak_achieved` events

These campaigns are configured entirely in the Console under **Retention > Push Campaigns** -- no code changes needed.

### View Feedback in the Console

Survey responses flow into the Console dashboard automatically:

1. **Feedback > Love Score** -- NPS/CSAT trends over time
2. **Feedback > Responses** -- individual responses with sentiment analysis
3. **Feedback > Alerts** -- get notified when NPS drops below a threshold

***

## Full Wired Example

**iOS**

```swift theme={null}
import AppDNASDK

class EngagementManager: AppDNAPushDelegate, AppDNASurveyDelegate {
    init() {
        AppDNA.pushModule.setDelegate(self)
        AppDNA.surveys.setDelegate(self)
    }

    // MARK: - Event Tracking

    func trackWorkoutCompleted(duration: Int, type: String, calories: Int) {
        AppDNA.track(event: "workout_completed", properties: [
            "duration_minutes": duration,
            "type": type,
            "calories": calories
        ])
    }

    func trackStreakAchieved(days: Int) {
        AppDNA.track(event: "streak_achieved", properties: ["streak_days": days])

        // Good moment to present the Console-configured review survey
        if days == 7 || days == 30 {
            AppDNA.surveys.present("smart_review")
        }
    }

    // MARK: - Push

    func onPushTapped(notification: PushPayload, actionId: String?) {
        guard let action = notification.action else { return }
        navigate(to: action.value)
    }

    func onPushReceived(notification: PushPayload, inForeground: Bool) {
        if inForeground {
            showBanner(title: notification.title, body: notification.body)
        }
    }

    // MARK: - Surveys

    func onSurveyCompleted(surveyId: String, responses: [SurveyResponse]) {
        guard let nps = responses.first(where: { $0.questionId == "nps_question" }),
              let score = nps.answer as? Int else { return }

        if score >= 9 {
            showReferralPrompt()
        } else if score <= 6 {
            showSupportLink()
        }
    }

    func onSurveyPresented(surveyId: String) { }
    func onSurveyDismissed(surveyId: String) { }
    func onPushTokenRegistered(token: String) { }
}
```

**Android**

```kotlin theme={null}
import ai.appdna.sdk.*

class EngagementManager : AppDNAPushDelegate, AppDNASurveyDelegate {
    init {
        AppDNA.push.setDelegate(this)
        AppDNA.surveys.setDelegate(this)
    }

    fun trackWorkoutCompleted(duration: Int, type: String, calories: Int) {
        AppDNA.track(
            event = "workout_completed",
            properties = mapOf("duration_minutes" to duration, "type" to type, "calories" to calories)
        )
    }

    fun trackStreakAchieved(days: Int) {
        AppDNA.track(event = "streak_achieved", properties = mapOf("streak_days" to days))
        if (days == 7 || days == 30) AppDNA.surveys.present("smart_review")
    }

    // Push
    override fun onPushTapped(notification: PushPayload, actionId: String?) {
        notification.action?.let { navigate(it.value) }
    }

    override fun onPushReceived(notification: PushPayload, inForeground: Boolean) {
        if (inForeground) showBanner(notification.title, notification.body)
    }

    // Surveys
    override fun onSurveyCompleted(surveyId: String, responses: List<SurveyResponse>) {
        val score = responses.firstOrNull { it.questionId == "nps_question" }?.answer as? Int ?: return
        when { score >= 9 -> showReferralPrompt(); score <= 6 -> showSupportLink() }
    }

    override fun onSurveyPresented(surveyId: String) { }
    override fun onSurveyDismissed(surveyId: String) { }
    override fun onPushTokenRegistered(token: String) { }
}
```

**Flutter**

```dart theme={null}
import 'package:appdna_sdk/appdna_sdk.dart';

// Dart abstract classes can't be mixed in, so use one standalone handler
// per delegate and register each with the matching module.
class EngagementManager {
  EngagementManager() {
    AppDNA.push.setDelegate(EngagementPushHandler());
    AppDNA.surveys.setDelegate(EngagementSurveyHandler());
  }

  Future<void> trackWorkoutCompleted(int duration, String type, int calories) async {
    await AppDNA.track("workout_completed", properties: {
      "duration_minutes": duration, "type": type, "calories": calories,
    });
  }

  Future<void> trackStreakAchieved(int days) async {
    await AppDNA.track("streak_achieved", properties: {"streak_days": days});
    if (days == 7 || days == 30) await AppDNA.surveys.present("smart_review");
  }
}

class EngagementPushHandler extends AppDNAPushDelegate {
  @override
  void onPushTapped(Map<String, dynamic> notification, String? actionId) {
    final value = notification["action_value"] as String?;
    if (value != null) navigate(value);
  }
}

class EngagementSurveyHandler extends AppDNASurveyDelegate {
  @override
  void onSurveyCompleted(String surveyId, List<Map<String, dynamic>> responses) {
    final nps = responses.firstWhere(
      (r) => r["questionId"] == "nps_question",
      orElse: () => {},
    );
    final score = (nps["answer"] as int?) ?? 0;
    if (score >= 9) showReferralPrompt();
    else if (score <= 6) showSupportLink();
  }
}
```

**React Native**

```typescript theme={null}
import { AppDNA } from "@appdna-ai/react-native-sdk";
import { useEffect } from "react";

function useEngagement() {
  useEffect(() => {
    AppDNA.push.setDelegate({
      onPushTokenRegistered: (token) => {},
      onPushReceived: (notification, inForeground) => {},
      onPushTapped: (notification) => {
        if (notification.action_value) navigate(notification.action_value as string);
      },
    });

    AppDNA.surveys.setDelegate({
      onSurveyPresented: (surveyId) => {},
      onSurveyDismissed: (surveyId) => {},
      onSurveyCompleted: (surveyId, responses) => {
        const score = (responses.find((r) => r.questionId === "nps_question")?.answer as number) ?? 0;
        if (score >= 9) showReferralPrompt();
        else if (score <= 6) showSupportLink();
      },
    });

    return () => {
      AppDNA.push.setDelegate(null);
      AppDNA.surveys.setDelegate(null);
    };
  }, []);

  function trackWorkout(duration: number, type: string, calories: number) {
    AppDNA.track("workout_completed", { duration_minutes: duration, type, calories });
  }

  async function trackStreak(days: number) {
    await AppDNA.track("streak_achieved", { streak_days: days });
    if (days === 7 || days === 30) await AppDNA.surveys.present("smart_review");
  }

  return { trackWorkout, trackStreak };
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.