Skip to content

CCCT-2447 Add Firebase Analytics For Add-Email Flows#3745

Merged
Jignesh-dimagi merged 6 commits into
masterfrom
ccct-2447-add-email-firebase-analytics
Jun 22, 2026
Merged

CCCT-2447 Add Firebase Analytics For Add-Email Flows#3745
Jignesh-dimagi merged 6 commits into
masterfrom
ccct-2447-add-email-firebase-analytics

Conversation

@Jignesh-dimagi

@Jignesh-dimagi Jignesh-dimagi commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

CCCT-2447

Technical Summary

Instruments the PersonalID add-email flows (signup / recovery / existing-user) with Firebase analytics. OTP request/verify reporting is centralized in EmailHelper.sendEmailOtp/verifyEmailOtp; the attempts count is threaded into verifyEmailOtp as a parameter. New email-specific OTP event types/method (OTP_EVENT_TYPE_REQUEST_EMAIL / VERIFY_EMAIL, OTP_METHOD_EMAIL) and OtpOp enum values reuse the existing reportOtpEvent path. Continue/skip events use reportPersonalIDContinueClicked for the existing-user offer prompt and the proceed-without-email path.

Safety Assurance

Safety story

What gives me confidence:

  • I ran the unit tests: the OtpAnalyticsMapper cases for the new email event types and the new EmailHelper emission tests (success/failure for both request and verify) all pass.
  • Narrow, additive diff — analytics calls only; no change to email send/verify behavior or control flow.
  • Request/verify reporting lives in a single chokepoint (EmailHelper), reducing the chance of missed or duplicated events.

Automated test coverage

  • OtpAnalyticsMapperTest: new cases asserting REQUEST_EMAILrequest_email and VERIFY_EMAILverify_email.
  • EmailHelperTest: new cases that capture the API callback and assert reportEmailOtpEvent is emitted with the correct op/outcome/reason/attempts on success and failure for both sendEmailOtp and verifyEmailOtp (including the threaded failedAttempts value).

Instrument the PersonalID add-email flows (signup / recovery /
existing-user) with Firebase analytics:
- Email OTP request/verify events via reportOtpEvent, with new
  OTP_EVENT_TYPE_REQUEST_EMAIL / VERIFY_EMAIL event types, an
  OTP_METHOD_EMAIL method, and new OtpOp enum values.
- Request/verify reporting centralized in EmailHelper so both the
  email-entry and OTP-verification fragments are covered without
  duplication.
- Continue/skip events via reportPersonalIDContinueClicked for the
  existing-user email offer prompt and the proceed-without-email path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Jignesh-dimagi Jignesh-dimagi self-assigned this Jun 2, 2026
@Jignesh-dimagi

Copy link
Copy Markdown
Contributor Author

Suggested Review Order

  • app/src/org/commcare/google/services/analytics/AnalyticsParamValue.java — new event-type/method constants the rest depend on
  • app/src/org/commcare/utils/OtpAnalyticsMapper.kt — new OtpOp values and their event-type mapping
  • app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.javareportEmailOtpEvent wrapper over reportOtpEvent
  • app/src/org/commcare/fragments/personalId/EmailHelper.kt — core integration: request/verify reporting centralized here
  • app/src/org/commcare/fragments/personalId/PersonalIdEmailVerificationFragment.kt — consumer; threads failedAttempts, reports proceed-without-email
  • app/src/org/commcare/connect/EmailOfferHelper.kt — consumer; offer-prompt add/skip events
  • app/unit-tests/src/org/commcare/utils/OtpAnalyticsMapperTest.kt — covers the new mappings
  • app/unit-tests/src/org/commcare/fragments/personalId/EmailHelperTest.kt — covers the emission behavior

@Jignesh-dimagi Jignesh-dimagi added the skip-integration-tests Skip android tests. label Jun 2, 2026
@Jignesh-dimagi Jignesh-dimagi marked this pull request as ready for review June 2, 2026 05:38
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds analytics instrumentation to email OTP flows in CommCare. It introduces new analytics constants for email OTP methods and event types, extends the OtpAnalyticsMapper enum with REQUEST_EMAIL and VERIFY_EMAIL variants, and implements a reportEmailOtpEvent helper in FirebaseAnalyticsUtil. EmailHelper now reports success/failure analytics for both email OTP request and verification operations, capturing outcome, failure reason, and failed attempt counts. EmailOfferHelper reports button click events for the email offer dialog. PersonalIdEmailVerificationFragment wires the failedAttempts counter through the verification flow and reports a "proceed without email" analytics event. Tests are updated to accommodate the new failedAttempts parameter and verify the new email event type mappings.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • dimagi/commcare-android#3741: The main PR's updates to EmailOfferHelper.kt add analytics events on the existing-user email offer dialog's "yes/no" button clicks, directly extending the email-offer dialog flow introduced in retrieved PR #3741.

Suggested labels

skip-integration-tests

Suggested reviewers

  • OrangeAndGreen
  • shubham1g5
  • conroy-ricketts
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding Firebase Analytics instrumentation to the PersonalID add-email flows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description covers all required template sections: Product Description (omitted as appropriate for analytics-only changes), Technical Summary with ticket link and design rationale, Safety Assurance with confidence story and automated test coverage details, and appropriate consideration of data impact.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ccct-2447-add-email-firebase-analytics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 25.94%. Comparing base (126182b) to head (5de6077).
⚠️ Report is 9 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3745      +/-   ##
============================================
+ Coverage     25.87%   25.94%   +0.06%     
- Complexity     4389     4405      +16     
============================================
  Files           950      951       +1     
  Lines         57186    57328     +142     
  Branches       6810     6821      +11     
============================================
+ Hits          14799    14875      +76     
- Misses        40558    40623      +65     
- Partials       1829     1830       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread app/src/org/commcare/google/services/analytics/FirebaseAnalyticsUtil.java Outdated
Comment thread app/src/org/commcare/fragments/personalId/PersonalIdEmailVerificationFragment.kt Outdated
activity.getString(R.string.personalid_email_offer_message),
)
dialog.setPositiveButton(activity.getString(R.string.personalid_email_offer_yes)) { _, _ ->
FirebaseAnalyticsUtil.reportPersonalIDContinueClicked(EMAIL_OFFER_SCREEN, null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

reportPersonalIDContinueClicked should only be used for Continue buttons on the Personal ID Sign up pages, I am afraid this will interfere with the existing funnel setup otherwise.

I would suggest logging this into a new event instead - email_offer_dialog and use it for both the email offer dialog and having trouble with email dialog.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To stay within Firebase event limits, we reuse a single "continue button clicked" event. Depending on the user's action, it logs the following payload:

  • Current Screen Name as SCREEN_NAME and null as PERSONAL_ID_CONTINUE_CLICKED_INFO when the user taps continue.
  • Current Screen Name as SCREEN_NAME and 'skip' as PERSONAL_ID_CONTINUE_CLICKED_INFO when the user taps skip.
  • 'EmailOfferDialog' as SCREEN_NAME and null as PERSONAL_ID_CONTINUE_CLICKED_INFO when an existing user chooses to add an email from the prompt.
  • 'EmailOfferDialog' as SCREEN_NAME and 'skip' as PERSONAL_ID_CONTINUE_CLICKED_INFO when an existing user chooses to skip adding an email from the prompt.
  • Current Screen Name as SCREEN_NAME and 'proceed_without_email' as PERSONAL_ID_CONTINUE_CLICKED_INFO when the user taps Proceed without email on the dialog.

Your thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it definitely come across as weird to me to use "Continue" button event to log "non" continue like events. Also this event is dedicated to track the Personal ID signup flow at the moment, I would highly encourage using a different event here on dialog confirmation CTAs otherwise it might be a mess to write a query tracking Personal ID funnel (for eg. when a continue button event appears in mid of a non-signup workflow.

Curious if @OrangeAndGreen have thoughts on this from writing a query perspective ?

Otherwise I would say that we should just add another generalised event here to track the email feature and log it with that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As far as querying goes, specifically related to the PersonalID funnel, I don't think this would cause issues. We aren't relying on this event in the funnel right now, and even if we start to we would pair it with the screen_name to know what page the user clicked Continue on, so the proposed new events could be easily filtered out since they wouldn't come from a page in the configuration workflow.

All in all I'm a bit torn. On the one side I can see Shubham's point that the usage of the event seems odd here, particularly in the "skip" case. But on the other hand I know we want to limit our usage of event_names in order to avoid the Firebase limit, so as long as we can reliably separate the data in queries then the proposed approach makes sense.

In the end, I think we're better off using a new event_name for these events. We have plenty of capacity for additional event_names, so while generally we should avoid creating new ones I think in this case it would be okay. When I think about it in terms of "Will we ever want to track these events together?" the answer seems to be no, i.e. the events for tracking Continue presses in the configuration workflow vs. other parts of the app would never be needed at the same time. So if the events are distinct that way, I think it makes sense for them to have different names.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your inputs @OrangeAndGreen @shubham1g5

I have added a new event, user_prompt. It is currently used in two places: email verification failure retries and existing user email reminders. However, because it is generic, it can also be used wherever a user's consent or decision is required in a prompt. One upcoming use case is in the profile management section, where a prompt will ask to send an OTP to an email address.

Changes done here

@conroy-ricketts conroy-ricketts left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great! Just had a few things to add

Comment thread app/src/org/commcare/utils/OtpAnalyticsMapper.kt Outdated
/**
* Sends an email OTP request
*/
fun sendEmailOtp(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know this function was created in a different PR, but can we rename this to requestEmailOtp()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 to this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All API handlers follow a naming convention that directs the server to take action. For example, "send" is named from the server's perspective—meaning the server sends the OTP to the user's email. Client methods simply pass the endpoint name up through every layer unchanged. Your thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good point although think that might be true for PersonalIdApiHandler methods but not for it's callers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah that is a good point

To play devil's advocate, the caller of PersonalIdApiHandler.sendPhoneOtp() uses the terminology "requestOtp" here, and that terminology is then reused going up through the OtpManager here, so there is an inconsistency there

But, definitely downgrading this point to a nit, especially because the Kdoc comment would help clear any potential confusion anyways

I'm cool with wherever you take this 👍🏾

Comment thread app/src/org/commcare/fragments/personalId/EmailHelper.kt Outdated
Comment thread app/src/org/commcare/fragments/personalId/EmailHelper.kt Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like a bit of over-engineering for a couple of counters. Thoughts on moving the counter variables to EmailHelper directly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since 'EmailHelper' is stateless, it cannot hold that data. The only remaining option was to provide the tracker from the source consuming these APIs. Now, it will work across all workflows: personal ID configuration, email offer prompt, and manage profile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That makes sense to me. I'm cool with keeping this as-is

shubham1g5
shubham1g5 previously approved these changes Jun 4, 2026

@shubham1g5 shubham1g5 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changing my review to non-blocking.

conroy-ricketts
conroy-ricketts previously approved these changes Jun 4, 2026
* {@link OtpAnalyticsMapper#workflowParam}. Logged to
* {@link CCAnalyticsParam#EMAIL_OTP_WORKFLOW}.
*/
public static void reportEmailOtpEvent(OtpAnalyticsMapper.OtpOp op,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function along with reportPhoneOtpEvent feel like they could use some cleanup:

  • Both usages rely on OtpAnalyticsMapper.getEventType to convert the OtpOp to a string, but the phone version relies on the fragment to call the helper (passing a string to analytics) while the email version passes the OtpOp to analytics and lets it call the helper. I think it makes sense for both version to pass the enum in and have analytics always be the one to call the helper function.
  • Generally (and after making the change above), I see a lot of overlap between the two functions, with the only real difference being that one logs a couple extra params (failed attempts count and email workflow). I'm thinking these could share a common base function: reportOtpEvent
  • Also to consider: Should the phone OTP workflow also log failed attempts? Maybe that's something we just didn't consider when implementing the phone version, and could be a nice improvement
  • Similarly, I think we could change EMAIL_OTP_WORKFLOW to just OTP_WORKFLOW, and for now hard-code REGISTRATION for phone-related calls since that's the only time a user does the phone verification. If/when we support allowing users to change their phone number, this code will be ready to support the analytics for the new workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Alright, I have updated the phone OTP logging to match the email OTP logging, which tracks the number of times the OTP is sent and the number of times verification fails. Additionally, I have created a common function to report these metrics.

Change done here

reportEvent(CCAnalyticsEvent.PERSONAL_ID_CONTINUE_CLICKED, params);
}

public static void reportUserPromptEvent(String type, String action, @Nullable String info) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the info can be non-nullable (no one ever passes null in)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Change done here

getString(R.string.personalid_email_otp_failed_message),
)
dialog.setPositiveButton(getString(R.string.personalid_email_otp_failed_retry)) { _, _ ->
FirebaseAnalyticsUtil.reportUserPromptEvent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Looking at these calls to reportUserPrompt, the ordering of the inputs feels odd. Seems like the "info" parameter would make more sense coming before the action, since it's kind of a sub-category of the workflow.
Like:

  • Email workflow, retrying after failure, user chooses to try again
  • Email workflow, retrying after failure, user chooses to proceed without email

public static final String EMAIL_OTP_WORKFLOW = "workflow";

// Param keys for user prompt analytics
static final String USER_PROMPT_TYPE = "type";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we make this string "event_type"? I'll start a convo with the team about this, but there's some benefit to re-using strings for similar types of fields across different events. We already have several similar parameters that map to "event_type", so making this new one do the same would be nice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Change done here

<argument
android:name="emailOtpRequestCount"
app:argType="integer"
android:defaultValue="0" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Curious why this argument is optional? I see it means we have to set it separately in PersonalIdEmailFragment (rather than including it with the other two args), and it doesn't seem like there's ever a case where we wouldn't pass the arg in

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was initially thinking of keeping it decoupled, but I agree with you that the user should have tried at least once before coming to this screen

Change done here

* - [requestCount]: total email OTP send/request calls this session.
* - [failedAttempts]: absolute number of failed email OTP verifications this session.
*/
class EmailOtpAttemptTracker(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: This could just be AttemptTracker, it doesn't need to be OTP and/or email specific.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Change done here

Comment thread app/src/org/commcare/fragments/personalId/AttemptTracker.kt
Comment thread app/src/org/commcare/fragments/personalId/AttemptTracker.kt Outdated
OrangeAndGreen
OrangeAndGreen previously approved these changes Jun 18, 2026
@Jignesh-dimagi Jignesh-dimagi merged commit 9d1b9b2 into master Jun 22, 2026
22 checks passed
@Jignesh-dimagi Jignesh-dimagi deleted the ccct-2447-add-email-firebase-analytics branch June 22, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-integration-tests Skip android tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants