From e3473985e9dca87b8eb1b5e4b59df667cb92388d Mon Sep 17 00:00:00 2001 From: wzhipan Date: Tue, 12 May 2026 11:53:07 -0700 Subject: [PATCH 1/4] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient Adds setOnboardingTelemetryRecorder() setter and emission calls at the WebView URL-handling sites identified in the design spec (Mobile Onboarding Telemetry section 7.5.2). Both the OneAuth fragment (non-brokered flow) and the broker AuthorizationActivity (brokered flow) can now attach a recorder so WebView page transitions are captured into the onboarding blob. Hook points (all best-effort, no-op if no recorder attached): - onPageFinished -> setLastLoadedDomain(host) - processInstallRequest / processIntentToInstallBrokerApp -> STEP_BROKER_INSTALL_PROMPTED - processDeviceCaRequest -> STEP_MDM_ENROLLMENT_STARTED - launchCompanyPortal -> STEP_COMPANY_PORTAL_LAUNCHED - processWebCpEnrollmentUrl -> STEP_WEB_CP_ENROLLMENT_STARTED - openGoogleEnrollmentUrl -> STEP_GOOGLE_ENROLLMENT_STARTED - processAuthAppMFAUrl -> STEP_AUTHENTICATOR_MFA_LINKING_STARTED Includes 4 new unit tests in AzureActiveDirectoryWebViewClientTest covering the broker install hook (with and without recorder) and onPageFinished domain recording (real URL and blank URL). All 59 WebView tests pass. Note: This branch also includes the constants commit (C4) so this branch compiles standalone. When both PRs land, the duplicate commit will resolve naturally on rebase. --- .../AzureActiveDirectoryWebViewClient.java | 81 ++++++++++++++++ ...AzureActiveDirectoryWebViewClientTest.java | 95 +++++++++++++++++++ 2 files changed, 176 insertions(+) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index 2cc939d3b8..747a13f5d1 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -154,6 +154,16 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient { private String mPasskeyRegistrationScript; + /** + * Optional onboarding telemetry recorder. Set via {@link #setOnboardingTelemetryRecorder} + * after this client is constructed (the recorder is created by the host fragment/activity + * when a seed JSON arrives, which is typically later than WebView construction). + * When non-null, key URL transitions (broker install, MDM enrollment, Company Portal + * launch, etc.) and `lastLoadedDomain` are recorded for the onboarding telemetry blob. + */ + @Nullable + private com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder mOnboardingTelemetryRecorder; + /** * Callback for tracking URL load events. */ @@ -201,11 +211,28 @@ public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final Str } } + /** + * Attach an onboarding telemetry recorder so subsequent WebView page transitions + * (broker install prompts, MDM enrollment redirects, Company Portal launches, etc.) + * are recorded into the onboarding telemetry blob. + * + * Recorder is owned by the host fragment / activity (e.g. OneAuthNavigationFragment + * on the OneAuth side, AuthorizationActivity on the broker side). May be null when + * no seed JSON is available — in which case all hooks become no-ops. + */ + public void setOnboardingTelemetryRecorder( + @Nullable final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder) { + mOnboardingTelemetryRecorder = recorder; + } + @Override public void onPageFinished(final WebView view, final String url) { super.onPageFinished(view, url); + // Onboarding telemetry: record domain navigation (best-effort, no-op if no recorder). + recordLastLoadedDomain(url); + if (mAuthUxJavaScriptInterfaceAdded) { // Add a function to the api. Must do this to first stringify the dict object, as Android @JavaScriptInterface does not support // passing dict objects through Javascript APIs, only Strings and primitive types. Server side will be sending message in a dict @@ -722,6 +749,9 @@ private void processDeviceCaRequest(@NonNull final WebView view, @NonNull final final String methodTag = TAG + ":processDeviceCaRequest"; Logger.info(methodTag, "This is a device CA request."); + // Onboarding telemetry: device CA blocking redirect → MDM enrollment phase. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED); + if (shouldLaunchCompanyPortal()) { // If CP is installed, redirect to CP. // TODO: Until we get a signal from eSTS that CP is the MDM app, we cannot assume that. @@ -831,6 +861,8 @@ private String getHomeTenantIdFromUrl(@NonNull final String url) { // This is a special case where the enrollment is not done in the WebView, but rather in the browser. private void processWebCpEnrollmentUrl(@NonNull final WebView view, @NonNull final String url) { final String methodTag = TAG + ":processWebCpEnrollmentUrl"; + // Onboarding telemetry: WebCP enrollment is a distinct enrollment path. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED); final Span span = createSpanWithAttributesFromParent(SpanName.ProcessWebCpEnrollmentRedirect.name()); try (final Scope scope = SpanExtension.makeCurrentSpan(span)) { view.stopLoading(); @@ -859,6 +891,8 @@ public void run() { // Opens the Google enrollment URL in the browser or the default intent handler (like DPC) private void openGoogleEnrollmentUrl(@NonNull final String url) { final String methodTag = TAG + ":openGoogleEnrollmentUrl"; + // Onboarding telemetry: Google enrollment redirect is a distinct enrollment path. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED); Logger.info(methodTag, "Opening Google enrollment URL"); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); @@ -920,6 +954,8 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N private void processAuthAppMFAUrl(String url) { final String methodTag = TAG + ":processAuthAppMFAUrl"; + // Onboarding telemetry: redirect to Authenticator for MFA linking. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED); Logger.verbose(methodTag, "Linking Account in Broker for MFA."); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); @@ -964,6 +1000,9 @@ void processAuthenticatorActivationAppLink(@NonNull final WebView view, private void launchCompanyPortal() { final String methodTag = TAG + ":launchCompanyPortal"; + // Onboarding telemetry: Company Portal launch is a discrete onboarding step. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED); + Logger.verbose(methodTag, "Sending intent to launch the CompanyPortal."); final Intent intent = new Intent(); intent.setComponent(new ComponentName( @@ -1051,6 +1090,10 @@ private void processWebCpRequest(@NonNull final WebView view, @NonNull final Str private void processInstallRequest(@NonNull final WebView view, @NonNull final String url) { final String methodTag = TAG + ":processInstallRequest"; + // Onboarding telemetry: broker install request reached the WebView client. Record the + // step at method entry so we capture intent regardless of the parsed result code. + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); + final RawAuthorizationResult result = RawAuthorizationResult.fromRedirectUri(url); if (result.getResultCode() != RawAuthorizationResult.ResultCode.BROKER_INSTALLATION_TRIGGERED) { @@ -1108,6 +1151,8 @@ private void processInvalidRedirectUri(@NonNull final WebView view, */ private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) { final String methodTag = TAG + ":processIntentToInstallBrokerApp"; + // Onboarding telemetry: alternate broker install path (intent-scheme). + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME); if (intent != null && intent.getPackage() != null) { @@ -1430,4 +1475,40 @@ private Span createSpanWithAttributesFromParent(@NonNull final String spanName) public void addPasskeyRegistrationJsScript(@NonNull final String script) { this.mPasskeyRegistrationScript = script; } + + /** + * Best-effort onboarding telemetry hook: records a step on the attached recorder + * if one is present. No-op when no recorder has been attached. Never throws. + */ + private void recordOnboardingStep(@NonNull final String stepId) { + final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; + if (recorder == null) { + return; + } + try { + recorder.addStep(stepId); + } catch (final Throwable t) { + Logger.warn(TAG, "Onboarding telemetry: failed to record step " + stepId + ": " + t.getMessage()); + } + } + + /** + * Best-effort onboarding telemetry hook: records the host of the most recently loaded + * page on the attached recorder. No-op when no recorder is attached, no host can be + * extracted, or url is null/blank. Never throws. + */ + private void recordLastLoadedDomain(@Nullable final String url) { + final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; + if (recorder == null || url == null || url.isEmpty()) { + return; + } + try { + final String host = Uri.parse(url).getHost(); + if (host != null && !host.isEmpty()) { + recorder.setLastLoadedDomain(host); + } + } catch (final Throwable t) { + Logger.warn(TAG, "Onboarding telemetry: failed to record last loaded domain: " + t.getMessage()); + } + } } diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java index cc4b5bcd51..082086c884 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java @@ -1095,4 +1095,99 @@ public void testProcessAuthAppMFAUrl_startsViewIntentWithNewTaskFlag() { assertTrue("MFA activation intent must carry FLAG_ACTIVITY_NEW_TASK", (launched.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0); } + + // ----------------------------------------------------------------------- + // Onboarding telemetry hooks + // ----------------------------------------------------------------------- + + /** + * Verifies that when an OnboardingTelemetryRecorder is attached to the WebView client, + * a broker install request URL produces a populated blob containing the + * {@code BrokerInstallPrompted} step. We construct a recorder with a synthetic seed + * containing a session correlation id and a blocking error so {@code finalizeBlob} + * returns non-empty. + */ + @Test + public void testProcessInstallRequest_RecordsBrokerInstallPromptedStep() throws Exception { + final String seedJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"non-brokered\"}"; + final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = + new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder( + seedJson, "client-id", "scope1", mContext); + // Record a blocking error so finalizeBlob() emits a populated blob. + recorder.addBlockingError( + com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL); + + mWebViewClient.setOnboardingTelemetryRecorder(recorder); + + // Trigger a broker install URL through the WebView client (delegates to processInstallRequest). + mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL); + + final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob()); + final org.json.JSONArray steps = blob.getJSONArray("steps_list"); + boolean foundStep = false; + for (int i = 0; i < steps.length(); i++) { + if ("BrokerInstallPrompted".equals(steps.getJSONObject(i).getString("step_id"))) { + foundStep = true; + break; + } + } + assertTrue("Expected BrokerInstallPrompted step in onboarding blob", foundStep); + } + + /** + * No recorder attached → no crash, hook is a no-op. + */ + @Test + public void testProcessInstallRequest_NoRecorder_IsNoOp() { + // Default mWebViewClient has no recorder attached. This must not throw. + assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL)); + } + + /** + * Verifies that {@code onPageFinished} extracts the host from a real URL and stores it on + * the recorder as {@code lastLoadedDomain}. Requires a blocking error to have been recorded + * so the finalized blob is non-empty. + */ + @Test + public void testOnPageFinished_RecordsLastLoadedDomain() throws Exception { + final String seedJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"non-brokered\"}"; + final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = + new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder( + seedJson, "client-id", "scope1", mContext); + recorder.addBlockingError( + com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL); + + mWebViewClient.setOnboardingTelemetryRecorder(recorder); + mWebViewClient.onPageFinished(mMockWebView, "https://login.microsoftonline.com/common/oauth2/authorize"); + + final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob()); + assertEquals("login.microsoftonline.com", blob.getString("last_loaded_domain")); + } + + /** + * onPageFinished with a URL that has no host (e.g. about:blank) does not throw and + * does not set lastLoadedDomain. + */ + @Test + public void testOnPageFinished_BlankUrl_DoesNotSetDomain() throws Exception { + final String seedJson = "{\"schema_version\":\"1.0.0\"," + + "\"session_correlation_id\":\"abc-123\"," + + "\"onboarding_mode\":\"non-brokered\"}"; + final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = + new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder( + seedJson, "client-id", "scope1", mContext); + recorder.addBlockingError( + com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL); + + mWebViewClient.setOnboardingTelemetryRecorder(recorder); + mWebViewClient.onPageFinished(mMockWebView, TEST_BLANK_PAGE_REQUEST_URL); + + final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob()); + assertFalse("blank URL should not produce a last_loaded_domain entry", + blob.has("last_loaded_domain")); + } } From bf6b1c10ab64a08d4ee77f08e343b4052f40b83e Mon Sep 17 00:00:00 2001 From: wzhipan Date: Mon, 18 May 2026 10:04:34 -0700 Subject: [PATCH 2/4] Changelog: webview onboarding telemetry hooks --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 6471f1c004..28d9b97fad 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient for page-transition step capture (broker install, MDM enrollment, Company Portal launch, MFA linking) and last-loaded-domain tracking - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) From c4819f89d7379d3a2a51d2e9b800afdedcfa271b Mon Sep 17 00:00:00 2001 From: wzhipan Date: Wed, 20 May 2026 11:47:29 -0700 Subject: [PATCH 3/4] Address review comments on PR #3121 - Use {@code lastLoadedDomain} in javadoc instead of markdown backticks - Emit STEP_BROKER_INSTALL_PROMPTED from processPlayStoreURL and processPlayStoreURLForBrokerApps so Play Store dispatch paths populate the onboarding step timeline (previously only processInstallRequest and processIntentToInstallBrokerApp did) - Test: use OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED and LAST_LOADED_DOMAIN constants instead of hardcoded string literals - Test: clear OnboardingSessionCorrelationStore in @After teardown to keep tests isolated from other onboarding tests --- .../webview/AzureActiveDirectoryWebViewClient.java | 4 +++- .../AzureActiveDirectoryWebViewClientTest.java | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index 747a13f5d1..2e25657369 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -159,7 +159,7 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient { * after this client is constructed (the recorder is created by the host fragment/activity * when a seed JSON arrives, which is typically later than WebView construction). * When non-null, key URL transitions (broker install, MDM enrollment, Company Portal - * launch, etc.) and `lastLoadedDomain` are recorded for the onboarding telemetry blob. + * launch, etc.) and {@code lastLoadedDomain} are recorded for the onboarding telemetry blob. */ @Nullable private com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder mOnboardingTelemetryRecorder; @@ -917,6 +917,7 @@ private boolean processPlayStoreURL(@NonNull final WebView view, @NonNull final } final String appPackageName = getBrokerAppPackageNameFromUrl(url); Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'"); + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_PREFIX + appPackageName)); @@ -936,6 +937,7 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N final String appPackageName = getBrokerAppPackageNameFromUrl(url); Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'"); + recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_APP_PREFIX + appPackageName)); diff --git a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java index 082086c884..2010203b69 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java +++ b/common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java @@ -183,6 +183,12 @@ public void onPageLoaded(final String url) { @After public void cleanUp(){ CommonFlightsManager.INSTANCE.resetFlightsManager(); + // Clear onboarding session-correlation SharedPreferences to keep tests isolated; + // OnboardingTelemetryRecorder.addBlockingError persists to this store. + if (mContext != null) { + new com.microsoft.identity.common.internal.telemetry.OnboardingSessionCorrelationStore(mContext) + .save(""); + } } @Test(expected = IllegalArgumentException.class) @@ -1128,7 +1134,8 @@ public void testProcessInstallRequest_RecordsBrokerInstallPromptedStep() throws final org.json.JSONArray steps = blob.getJSONArray("steps_list"); boolean foundStep = false; for (int i = 0; i < steps.length(); i++) { - if ("BrokerInstallPrompted".equals(steps.getJSONObject(i).getString("step_id"))) { + if (com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants + .STEP_BROKER_INSTALL_PROMPTED.equals(steps.getJSONObject(i).getString("step_id"))) { foundStep = true; break; } @@ -1165,7 +1172,8 @@ public void testOnPageFinished_RecordsLastLoadedDomain() throws Exception { mWebViewClient.onPageFinished(mMockWebView, "https://login.microsoftonline.com/common/oauth2/authorize"); final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob()); - assertEquals("login.microsoftonline.com", blob.getString("last_loaded_domain")); + assertEquals("login.microsoftonline.com", blob.getString( + com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.LAST_LOADED_DOMAIN)); } /** From 2a29e0b28e0fc6abcc8f718de632f9e65133d8f6 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Wed, 20 May 2026 14:29:29 -0700 Subject: [PATCH 4/4] Address Praveen's follow-up review on PR #3121 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace fully-qualified telemetry class names with imports (regular import for OnboardingTelemetryRecorder, static imports for the STEP_* constants) so call sites read cleanly - recordLastLoadedDomain: change url parameter from @Nullable to @NonNull to match WebViewClient.onPageFinished contract; drop the null check - recordLastLoadedDomain: log Logger.verbose when Uri.parse(url).getHost() returns null/empty for a non-empty URL — aids diagnostics for malformed URLs without being noisy in production - Changelog: add (#3121) PR reference suffix --- changelog.txt | 2 +- .../AzureActiveDirectoryWebViewClient.java | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index 28d9b97fad..5287b4abf3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ vNext ---------- -- [MINOR] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient for page-transition step capture (broker install, MDM enrollment, Company Portal launch, MFA linking) and last-loaded-domain tracking +- [MINOR] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient for page-transition step capture (broker install, MDM enrollment, Company Portal launch, MFA linking) and last-loaded-domain tracking (#3121) - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java index 2e25657369..4177e72dee 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java @@ -79,11 +79,18 @@ import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge; import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory; +import com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder; import com.microsoft.identity.common.internal.ui.webview.challengehandlers.PKeyAuthChallengeHandler; import com.microsoft.identity.common.java.WarningType; import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.exception.ErrorStrings; import com.microsoft.identity.common.java.providers.RawAuthorizationResult; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED; +import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED; import com.microsoft.identity.common.java.util.StringUtil; import com.microsoft.identity.common.logging.Logger; @@ -162,7 +169,7 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient { * launch, etc.) and {@code lastLoadedDomain} are recorded for the onboarding telemetry blob. */ @Nullable - private com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder mOnboardingTelemetryRecorder; + private OnboardingTelemetryRecorder mOnboardingTelemetryRecorder; /** * Callback for tracking URL load events. @@ -221,7 +228,7 @@ public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final Str * no seed JSON is available — in which case all hooks become no-ops. */ public void setOnboardingTelemetryRecorder( - @Nullable final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder) { + @Nullable final OnboardingTelemetryRecorder recorder) { mOnboardingTelemetryRecorder = recorder; } @@ -750,7 +757,7 @@ private void processDeviceCaRequest(@NonNull final WebView view, @NonNull final Logger.info(methodTag, "This is a device CA request."); // Onboarding telemetry: device CA blocking redirect → MDM enrollment phase. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED); + recordOnboardingStep(STEP_MDM_ENROLLMENT_STARTED); if (shouldLaunchCompanyPortal()) { // If CP is installed, redirect to CP. @@ -862,7 +869,7 @@ private String getHomeTenantIdFromUrl(@NonNull final String url) { private void processWebCpEnrollmentUrl(@NonNull final WebView view, @NonNull final String url) { final String methodTag = TAG + ":processWebCpEnrollmentUrl"; // Onboarding telemetry: WebCP enrollment is a distinct enrollment path. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED); + recordOnboardingStep(STEP_WEB_CP_ENROLLMENT_STARTED); final Span span = createSpanWithAttributesFromParent(SpanName.ProcessWebCpEnrollmentRedirect.name()); try (final Scope scope = SpanExtension.makeCurrentSpan(span)) { view.stopLoading(); @@ -892,7 +899,7 @@ public void run() { private void openGoogleEnrollmentUrl(@NonNull final String url) { final String methodTag = TAG + ":openGoogleEnrollmentUrl"; // Onboarding telemetry: Google enrollment redirect is a distinct enrollment path. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED); + recordOnboardingStep(STEP_GOOGLE_ENROLLMENT_STARTED); Logger.info(methodTag, "Opening Google enrollment URL"); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); @@ -917,7 +924,7 @@ private boolean processPlayStoreURL(@NonNull final WebView view, @NonNull final } final String appPackageName = getBrokerAppPackageNameFromUrl(url); Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'"); - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); + recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_PREFIX + appPackageName)); @@ -937,7 +944,7 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N final String appPackageName = getBrokerAppPackageNameFromUrl(url); Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'"); - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); + recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_APP_PREFIX + appPackageName)); @@ -957,7 +964,7 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N private void processAuthAppMFAUrl(String url) { final String methodTag = TAG + ":processAuthAppMFAUrl"; // Onboarding telemetry: redirect to Authenticator for MFA linking. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED); + recordOnboardingStep(STEP_AUTHENTICATOR_MFA_LINKING_STARTED); Logger.verbose(methodTag, "Linking Account in Broker for MFA."); try { final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); @@ -1003,7 +1010,7 @@ private void launchCompanyPortal() { final String methodTag = TAG + ":launchCompanyPortal"; // Onboarding telemetry: Company Portal launch is a discrete onboarding step. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED); + recordOnboardingStep(STEP_COMPANY_PORTAL_LAUNCHED); Logger.verbose(methodTag, "Sending intent to launch the CompanyPortal."); final Intent intent = new Intent(); @@ -1094,7 +1101,7 @@ private void processInstallRequest(@NonNull final WebView view, @NonNull final S // Onboarding telemetry: broker install request reached the WebView client. Record the // step at method entry so we capture intent regardless of the parsed result code. - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); + recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED); final RawAuthorizationResult result = RawAuthorizationResult.fromRedirectUri(url); @@ -1154,7 +1161,7 @@ private void processInvalidRedirectUri(@NonNull final WebView view, private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) { final String methodTag = TAG + ":processIntentToInstallBrokerApp"; // Onboarding telemetry: alternate broker install path (intent-scheme). - recordOnboardingStep(com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED); + recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED); try { final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME); if (intent != null && intent.getPackage() != null) { @@ -1483,7 +1490,7 @@ public void addPasskeyRegistrationJsScript(@NonNull final String script) { * if one is present. No-op when no recorder has been attached. Never throws. */ private void recordOnboardingStep(@NonNull final String stepId) { - final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; + final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; if (recorder == null) { return; } @@ -1496,18 +1503,20 @@ private void recordOnboardingStep(@NonNull final String stepId) { /** * Best-effort onboarding telemetry hook: records the host of the most recently loaded - * page on the attached recorder. No-op when no recorder is attached, no host can be - * extracted, or url is null/blank. Never throws. + * page on the attached recorder. No-op when no recorder is attached or the URL has no + * extractable host. Never throws. */ - private void recordLastLoadedDomain(@Nullable final String url) { - final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; - if (recorder == null || url == null || url.isEmpty()) { + private void recordLastLoadedDomain(@NonNull final String url) { + final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder; + if (recorder == null || url.isEmpty()) { return; } try { final String host = Uri.parse(url).getHost(); if (host != null && !host.isEmpty()) { recorder.setLastLoadedDomain(host); + } else { + Logger.verbose(TAG, "Onboarding telemetry: no host extracted from URL"); } } catch (final Throwable t) { Logger.warn(TAG, "Onboarding telemetry: failed to record last loaded domain: " + t.getMessage());