Skip to content

Commit cf1f134

Browse files
authored
Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient, Fixes AB#3462876 (#3121)
**C3** in the onboarding telemetry feature series (follows merged #3088 / #3111 / #3117). Adds optional onboarding-telemetry instrumentation to `AzureActiveDirectoryWebViewClient` so the WebView page-transition flow can populate the onboarding blob. **What this adds:** - `setOnboardingTelemetryRecorder(recorder)` setter on `AzureActiveDirectoryWebViewClient` — host fragment/activity attaches the recorder once a seed JSON arrives. - Step emissions at the existing URL-dispatch sites (no behavior change to those paths): - `processInstallRequest` / `processPlayStoreURL` / `processIntentToInstallBrokerApp` → `STEP_BROKER_INSTALL_PROMPTED` - `processDeviceCaRequest` → `STEP_MDM_ENROLLMENT_STARTED` - `launchCompanyPortal` / `processWebCpRequest` → `STEP_COMPANY_PORTAL_LAUNCHED` - `processWebCpEnrollmentUrl` → `STEP_WEB_CP_ENROLLMENT_STARTED` - `openGoogleEnrollmentUrl` → `STEP_GOOGLE_ENROLLMENT_STARTED` - `processAuthAppMFAUrl` → `STEP_AUTHENTICATOR_MFA_LINKING_STARTED` - `onPageFinished` captures the host of the final loaded URL into `last_loaded_domain`. **Safety:** Recorder is `@Nullable` — when not attached (every existing caller, today), this is a no-op. **Tests:** `AzureActiveDirectoryWebViewClientTest` covers all setter + emission paths. Fixes [AB#3462876](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3462876)
1 parent 0a72116 commit cf1f134

3 files changed

Lines changed: 196 additions & 0 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ vNext
33
- [PATCH] Emit ipc_strategy telemetry attribute for successful device registration IPC strategy and refactor execute flow to pack protocol request once before strategy retries (#3124)
44
- [PATCH] Fix Edge browser selection on devices where Microsoft Edge is the default browser: add the rotated Edge signing certificate hash to the Edge BrowserDescriptor and accept multi-signer browsers when any signature intersects the safelist, instead of requiring strict set-equality (resolves MSAL #2414)
55
- [MINOR] Refactor Auth Tab integration to use provider-based strategy selection. Adds AuthTabStrategyProvider and BrowserLaunchStrategy with Custom Tabs fallback. Compatible with androidx.browser:browser:1.7.0.
6+
- [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)
67
- [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086)
78
- [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114)
89
- [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109)

common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,18 @@
7979
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
8080
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge;
8181
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory;
82+
import com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder;
8283
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.PKeyAuthChallengeHandler;
8384
import com.microsoft.identity.common.java.WarningType;
8485
import com.microsoft.identity.common.java.exception.ClientException;
8586
import com.microsoft.identity.common.java.exception.ErrorStrings;
8687
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
88+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED;
89+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED;
90+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED;
91+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED;
92+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED;
93+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED;
8794
import com.microsoft.identity.common.java.util.StringUtil;
8895
import com.microsoft.identity.common.logging.Logger;
8996

@@ -154,6 +161,16 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient {
154161

155162
private String mPasskeyRegistrationScript;
156163

164+
/**
165+
* Optional onboarding telemetry recorder. Set via {@link #setOnboardingTelemetryRecorder}
166+
* after this client is constructed (the recorder is created by the host fragment/activity
167+
* when a seed JSON arrives, which is typically later than WebView construction).
168+
* When non-null, key URL transitions (broker install, MDM enrollment, Company Portal
169+
* launch, etc.) and {@code lastLoadedDomain} are recorded for the onboarding telemetry blob.
170+
*/
171+
@Nullable
172+
private OnboardingTelemetryRecorder mOnboardingTelemetryRecorder;
173+
157174
/**
158175
* Callback for tracking URL load events.
159176
*/
@@ -201,11 +218,28 @@ public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final Str
201218
}
202219
}
203220

221+
/**
222+
* Attach an onboarding telemetry recorder so subsequent WebView page transitions
223+
* (broker install prompts, MDM enrollment redirects, Company Portal launches, etc.)
224+
* are recorded into the onboarding telemetry blob.
225+
*
226+
* Recorder is owned by the host fragment / activity (e.g. OneAuthNavigationFragment
227+
* on the OneAuth side, AuthorizationActivity on the broker side). May be null when
228+
* no seed JSON is available — in which case all hooks become no-ops.
229+
*/
230+
public void setOnboardingTelemetryRecorder(
231+
@Nullable final OnboardingTelemetryRecorder recorder) {
232+
mOnboardingTelemetryRecorder = recorder;
233+
}
234+
204235
@Override
205236
public void onPageFinished(final WebView view,
206237
final String url) {
207238
super.onPageFinished(view, url);
208239

240+
// Onboarding telemetry: record domain navigation (best-effort, no-op if no recorder).
241+
recordLastLoadedDomain(url);
242+
209243
if (mAuthUxJavaScriptInterfaceAdded) {
210244
// Add a function to the api. Must do this to first stringify the dict object, as Android @JavaScriptInterface does not support
211245
// passing dict objects through Javascript APIs, only Strings and primitive types. Server side will be sending message in a dict
@@ -722,6 +756,9 @@ private void processDeviceCaRequest(@NonNull final WebView view, @NonNull final
722756
final String methodTag = TAG + ":processDeviceCaRequest";
723757
Logger.info(methodTag, "This is a device CA request.");
724758

759+
// Onboarding telemetry: device CA blocking redirect → MDM enrollment phase.
760+
recordOnboardingStep(STEP_MDM_ENROLLMENT_STARTED);
761+
725762
if (shouldLaunchCompanyPortal()) {
726763
// If CP is installed, redirect to CP.
727764
// TODO: Until we get a signal from eSTS that CP is the MDM app, we cannot assume that.
@@ -831,6 +868,8 @@ private String getHomeTenantIdFromUrl(@NonNull final String url) {
831868
// This is a special case where the enrollment is not done in the WebView, but rather in the browser.
832869
private void processWebCpEnrollmentUrl(@NonNull final WebView view, @NonNull final String url) {
833870
final String methodTag = TAG + ":processWebCpEnrollmentUrl";
871+
// Onboarding telemetry: WebCP enrollment is a distinct enrollment path.
872+
recordOnboardingStep(STEP_WEB_CP_ENROLLMENT_STARTED);
834873
final Span span = createSpanWithAttributesFromParent(SpanName.ProcessWebCpEnrollmentRedirect.name());
835874
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
836875
view.stopLoading();
@@ -859,6 +898,8 @@ public void run() {
859898
// Opens the Google enrollment URL in the browser or the default intent handler (like DPC)
860899
private void openGoogleEnrollmentUrl(@NonNull final String url) {
861900
final String methodTag = TAG + ":openGoogleEnrollmentUrl";
901+
// Onboarding telemetry: Google enrollment redirect is a distinct enrollment path.
902+
recordOnboardingStep(STEP_GOOGLE_ENROLLMENT_STARTED);
862903
Logger.info(methodTag, "Opening Google enrollment URL");
863904
try {
864905
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@@ -883,6 +924,7 @@ private boolean processPlayStoreURL(@NonNull final WebView view, @NonNull final
883924
}
884925
final String appPackageName = getBrokerAppPackageNameFromUrl(url);
885926
Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'");
927+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
886928

887929
try {
888930
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_PREFIX + appPackageName));
@@ -902,6 +944,7 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N
902944

903945
final String appPackageName = getBrokerAppPackageNameFromUrl(url);
904946
Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'");
947+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
905948

906949
try {
907950
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_APP_PREFIX + appPackageName));
@@ -920,6 +963,8 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N
920963

921964
private void processAuthAppMFAUrl(String url) {
922965
final String methodTag = TAG + ":processAuthAppMFAUrl";
966+
// Onboarding telemetry: redirect to Authenticator for MFA linking.
967+
recordOnboardingStep(STEP_AUTHENTICATOR_MFA_LINKING_STARTED);
923968
Logger.verbose(methodTag, "Linking Account in Broker for MFA.");
924969
try {
925970
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@@ -964,6 +1009,9 @@ void processAuthenticatorActivationAppLink(@NonNull final WebView view,
9641009
private void launchCompanyPortal() {
9651010
final String methodTag = TAG + ":launchCompanyPortal";
9661011

1012+
// Onboarding telemetry: Company Portal launch is a discrete onboarding step.
1013+
recordOnboardingStep(STEP_COMPANY_PORTAL_LAUNCHED);
1014+
9671015
Logger.verbose(methodTag, "Sending intent to launch the CompanyPortal.");
9681016
final Intent intent = new Intent();
9691017
intent.setComponent(new ComponentName(
@@ -1051,6 +1099,10 @@ private void processWebCpRequest(@NonNull final WebView view, @NonNull final Str
10511099
private void processInstallRequest(@NonNull final WebView view, @NonNull final String url) {
10521100
final String methodTag = TAG + ":processInstallRequest";
10531101

1102+
// Onboarding telemetry: broker install request reached the WebView client. Record the
1103+
// step at method entry so we capture intent regardless of the parsed result code.
1104+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
1105+
10541106
final RawAuthorizationResult result = RawAuthorizationResult.fromRedirectUri(url);
10551107

10561108
if (result.getResultCode() != RawAuthorizationResult.ResultCode.BROKER_INSTALLATION_TRIGGERED) {
@@ -1108,6 +1160,8 @@ private void processInvalidRedirectUri(@NonNull final WebView view,
11081160
*/
11091161
private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) {
11101162
final String methodTag = TAG + ":processIntentToInstallBrokerApp";
1163+
// Onboarding telemetry: alternate broker install path (intent-scheme).
1164+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
11111165
try {
11121166
final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
11131167
if (intent != null && intent.getPackage() != null) {
@@ -1430,4 +1484,42 @@ private Span createSpanWithAttributesFromParent(@NonNull final String spanName)
14301484
public void addPasskeyRegistrationJsScript(@NonNull final String script) {
14311485
this.mPasskeyRegistrationScript = script;
14321486
}
1487+
1488+
/**
1489+
* Best-effort onboarding telemetry hook: records a step on the attached recorder
1490+
* if one is present. No-op when no recorder has been attached. Never throws.
1491+
*/
1492+
private void recordOnboardingStep(@NonNull final String stepId) {
1493+
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
1494+
if (recorder == null) {
1495+
return;
1496+
}
1497+
try {
1498+
recorder.addStep(stepId);
1499+
} catch (final Throwable t) {
1500+
Logger.warn(TAG, "Onboarding telemetry: failed to record step " + stepId + ": " + t.getMessage());
1501+
}
1502+
}
1503+
1504+
/**
1505+
* Best-effort onboarding telemetry hook: records the host of the most recently loaded
1506+
* page on the attached recorder. No-op when no recorder is attached or the URL has no
1507+
* extractable host. Never throws.
1508+
*/
1509+
private void recordLastLoadedDomain(@NonNull final String url) {
1510+
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
1511+
if (recorder == null || url.isEmpty()) {
1512+
return;
1513+
}
1514+
try {
1515+
final String host = Uri.parse(url).getHost();
1516+
if (host != null && !host.isEmpty()) {
1517+
recorder.setLastLoadedDomain(host);
1518+
} else {
1519+
Logger.verbose(TAG, "Onboarding telemetry: no host extracted from URL");
1520+
}
1521+
} catch (final Throwable t) {
1522+
Logger.warn(TAG, "Onboarding telemetry: failed to record last loaded domain: " + t.getMessage());
1523+
}
1524+
}
14331525
}

common/src/test/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClientTest.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ public void onPageLoaded(final String url) {
183183
@After
184184
public void cleanUp(){
185185
CommonFlightsManager.INSTANCE.resetFlightsManager();
186+
// Clear onboarding session-correlation SharedPreferences to keep tests isolated;
187+
// OnboardingTelemetryRecorder.addBlockingError persists to this store.
188+
if (mContext != null) {
189+
new com.microsoft.identity.common.internal.telemetry.OnboardingSessionCorrelationStore(mContext)
190+
.save("");
191+
}
186192
}
187193

188194
@Test(expected = IllegalArgumentException.class)
@@ -1095,4 +1101,101 @@ public void testProcessAuthAppMFAUrl_startsViewIntentWithNewTaskFlag() {
10951101
assertTrue("MFA activation intent must carry FLAG_ACTIVITY_NEW_TASK",
10961102
(launched.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0);
10971103
}
1104+
1105+
// -----------------------------------------------------------------------
1106+
// Onboarding telemetry hooks
1107+
// -----------------------------------------------------------------------
1108+
1109+
/**
1110+
* Verifies that when an OnboardingTelemetryRecorder is attached to the WebView client,
1111+
* a broker install request URL produces a populated blob containing the
1112+
* {@code BrokerInstallPrompted} step. We construct a recorder with a synthetic seed
1113+
* containing a session correlation id and a blocking error so {@code finalizeBlob}
1114+
* returns non-empty.
1115+
*/
1116+
@Test
1117+
public void testProcessInstallRequest_RecordsBrokerInstallPromptedStep() throws Exception {
1118+
final String seedJson = "{\"schema_version\":\"1.0.0\","
1119+
+ "\"session_correlation_id\":\"abc-123\","
1120+
+ "\"onboarding_mode\":\"non-brokered\"}";
1121+
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
1122+
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
1123+
seedJson, "client-id", "scope1", mContext);
1124+
// Record a blocking error so finalizeBlob() emits a populated blob.
1125+
recorder.addBlockingError(
1126+
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);
1127+
1128+
mWebViewClient.setOnboardingTelemetryRecorder(recorder);
1129+
1130+
// Trigger a broker install URL through the WebView client (delegates to processInstallRequest).
1131+
mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL);
1132+
1133+
final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
1134+
final org.json.JSONArray steps = blob.getJSONArray("steps_list");
1135+
boolean foundStep = false;
1136+
for (int i = 0; i < steps.length(); i++) {
1137+
if (com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants
1138+
.STEP_BROKER_INSTALL_PROMPTED.equals(steps.getJSONObject(i).getString("step_id"))) {
1139+
foundStep = true;
1140+
break;
1141+
}
1142+
}
1143+
assertTrue("Expected BrokerInstallPrompted step in onboarding blob", foundStep);
1144+
}
1145+
1146+
/**
1147+
* No recorder attached → no crash, hook is a no-op.
1148+
*/
1149+
@Test
1150+
public void testProcessInstallRequest_NoRecorder_IsNoOp() {
1151+
// Default mWebViewClient has no recorder attached. This must not throw.
1152+
assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL));
1153+
}
1154+
1155+
/**
1156+
* Verifies that {@code onPageFinished} extracts the host from a real URL and stores it on
1157+
* the recorder as {@code lastLoadedDomain}. Requires a blocking error to have been recorded
1158+
* so the finalized blob is non-empty.
1159+
*/
1160+
@Test
1161+
public void testOnPageFinished_RecordsLastLoadedDomain() throws Exception {
1162+
final String seedJson = "{\"schema_version\":\"1.0.0\","
1163+
+ "\"session_correlation_id\":\"abc-123\","
1164+
+ "\"onboarding_mode\":\"non-brokered\"}";
1165+
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
1166+
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
1167+
seedJson, "client-id", "scope1", mContext);
1168+
recorder.addBlockingError(
1169+
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);
1170+
1171+
mWebViewClient.setOnboardingTelemetryRecorder(recorder);
1172+
mWebViewClient.onPageFinished(mMockWebView, "https://login.microsoftonline.com/common/oauth2/authorize");
1173+
1174+
final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
1175+
assertEquals("login.microsoftonline.com", blob.getString(
1176+
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.LAST_LOADED_DOMAIN));
1177+
}
1178+
1179+
/**
1180+
* onPageFinished with a URL that has no host (e.g. about:blank) does not throw and
1181+
* does not set lastLoadedDomain.
1182+
*/
1183+
@Test
1184+
public void testOnPageFinished_BlankUrl_DoesNotSetDomain() throws Exception {
1185+
final String seedJson = "{\"schema_version\":\"1.0.0\","
1186+
+ "\"session_correlation_id\":\"abc-123\","
1187+
+ "\"onboarding_mode\":\"non-brokered\"}";
1188+
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
1189+
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
1190+
seedJson, "client-id", "scope1", mContext);
1191+
recorder.addBlockingError(
1192+
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);
1193+
1194+
mWebViewClient.setOnboardingTelemetryRecorder(recorder);
1195+
mWebViewClient.onPageFinished(mMockWebView, TEST_BLANK_PAGE_REQUEST_URL);
1196+
1197+
final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
1198+
assertFalse("blank URL should not produce a last_loaded_domain entry",
1199+
blob.has("last_loaded_domain"));
1200+
}
10981201
}

0 commit comments

Comments
 (0)