Skip to content

Commit 7c14fbb

Browse files
committed
Add OnboardingBlobBuilder, FieldKeys, and SessionCachePersistence for onboarding telemetry
1 parent 4539cfa commit 7c14fbb

3 files changed

Lines changed: 352 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.identity.common.internal.telemetry;
5+
6+
import android.content.Context;
7+
import android.content.SharedPreferences;
8+
9+
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
11+
12+
import com.microsoft.identity.common.java.telemetry.OnboardingBlobFieldKeys;
13+
14+
import org.json.JSONArray;
15+
import org.json.JSONException;
16+
import org.json.JSONObject;
17+
18+
import java.time.Instant;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
/**
23+
* Android blob builder for onboarding telemetry.
24+
* Called by WebView navigation fragments during interactive auth flows.
25+
* Operates on an in-memory Java model, converts to JSON at flow end.
26+
* On block detection, persists sessionCorrelationId to SharedPreferences
27+
* immediately for app-kill resilience.
28+
*
29+
* Lives in Android common core so both OneAuth (non-brokered) and broker apps
30+
* (brokered) use the same class.
31+
*/
32+
public class OnboardingBlobBuilder {
33+
34+
private static final String PREFS_FILE = "com.microsoft.oneauth.session_correlation_cache";
35+
private static final String TAG = "OnboardingBlobBuilder";
36+
37+
// Seed field key constants — must match OnboardingBlobConstants (Djinni-generated).
38+
// Duplicated here to avoid a dependency on the Djinni-generated Java class in Common.
39+
private static final String FIELD_SCHEMA_VERSION = "schema_version";
40+
private static final String FIELD_SESSION_CORRELATION_ID = "sessionCorrelationId";
41+
private static final String FIELD_ONBOARDING_MODE = "onboardingMode";
42+
private static final String FIELD_STEPS_LIST = "stepsList";
43+
private static final String FIELD_STEP_ID = "stepId";
44+
private static final String FIELD_TS = "ts";
45+
46+
// Seed fields (from C++ common core)
47+
private final String mSchemaVersion;
48+
private final String mSessionCorrelationId;
49+
private final String mOnboardingMode;
50+
51+
// Builder identity for persistence
52+
private final String mClientId;
53+
private final String mTarget;
54+
private final Context mContext;
55+
56+
// Populated fields
57+
private final List<StepEntry> mStepsList = new ArrayList<>();
58+
private final List<String> mBlockingErrors = new ArrayList<>();
59+
private String mLastLoadedDomain;
60+
private boolean mRemediationNeeded;
61+
private String mProfile;
62+
private final List<String> mUxFlowUsed = new ArrayList<>();
63+
64+
private static class StepEntry {
65+
final String stepId;
66+
final String timestamp; // ISO 8601
67+
68+
StepEntry(String stepId, String timestamp) {
69+
this.stepId = stepId;
70+
this.timestamp = timestamp;
71+
}
72+
}
73+
74+
/**
75+
* Construct a builder from the seed JSON provided by C++ InteractiveRequest.
76+
*
77+
* @param seedJson The seed blob JSON string from authParameters
78+
* @param clientId Client ID for cache persistence key
79+
* @param target Target (scopes) for cache persistence key
80+
* @param context Android context for SharedPreferences access
81+
*/
82+
public OnboardingBlobBuilder(
83+
@NonNull String seedJson,
84+
@NonNull String clientId,
85+
@NonNull String target,
86+
@NonNull Context context) {
87+
mClientId = clientId;
88+
mTarget = target;
89+
mContext = context.getApplicationContext();
90+
91+
// Deserialize seed JSON
92+
String schemaVersion = "";
93+
String sessionCorrelationId = "";
94+
String onboardingMode = "";
95+
try {
96+
JSONObject seed = new JSONObject(seedJson);
97+
schemaVersion = seed.optString(FIELD_SCHEMA_VERSION, "");
98+
sessionCorrelationId = seed.optString(FIELD_SESSION_CORRELATION_ID, "");
99+
onboardingMode = seed.optString(FIELD_ONBOARDING_MODE, "");
100+
} catch (JSONException e) {
101+
// Corrupted seed — use empty values
102+
}
103+
mSchemaVersion = schemaVersion;
104+
mSessionCorrelationId = sessionCorrelationId;
105+
mOnboardingMode = onboardingMode;
106+
}
107+
108+
/**
109+
* Record a step in the onboarding flow.
110+
*
111+
* @param stepId Step ID constant (from OnboardingBlobFieldKeys)
112+
* @param timestamp The time when the step occurred
113+
*/
114+
public void addStep(@NonNull String stepId, @NonNull Instant timestamp) {
115+
mStepsList.add(new StepEntry(stepId, timestamp.toString()));
116+
}
117+
118+
/**
119+
* Record a blocking error code detected during the flow.
120+
* Also persists the session correlation entry to SharedPreferences immediately
121+
* for app-kill resilience.
122+
*
123+
* @param errorCode The blocking error code (e.g., "65001", "53000")
124+
*/
125+
public void addBlockingError(@NonNull String errorCode) {
126+
mBlockingErrors.add(errorCode);
127+
mRemediationNeeded = true;
128+
129+
// Persist session correlation to SharedPreferences immediately on block
130+
persistSessionCorrelation();
131+
}
132+
133+
/**
134+
* Set the last loaded domain during WebView navigation.
135+
*
136+
* @param domain The domain URL (e.g., "login.microsoftonline.com")
137+
*/
138+
public void setLastLoadedDomain(@NonNull String domain) {
139+
mLastLoadedDomain = domain;
140+
}
141+
142+
/**
143+
* Set whether remediation is needed.
144+
*
145+
* @param needed True if CA remediation is required
146+
*/
147+
public void setRemediationNeeded(boolean needed) {
148+
mRemediationNeeded = needed;
149+
}
150+
151+
/**
152+
* Set the Android profile context.
153+
*
154+
* @param profile One of OnboardingBlobFieldKeys.PROFILE_USER or PROFILE_WORK
155+
*/
156+
public void setProfile(@NonNull String profile) {
157+
mProfile = profile;
158+
}
159+
160+
/**
161+
* Add a UX flow variant tag.
162+
*
163+
* @param flowTag Flow variant (e.g., "MobileOnboardingPhase1")
164+
*/
165+
public void addUxFlowUsed(@NonNull String flowTag) {
166+
mUxFlowUsed.add(flowTag);
167+
}
168+
169+
/**
170+
* Finalize the blob and return the JSON string.
171+
* If no blocking errors were recorded, returns empty string (clears seed blob).
172+
* Otherwise serializes the populated blob to JSON.
173+
*
174+
* @return Populated blob JSON string, or empty string if no blocking errors
175+
*/
176+
@NonNull
177+
public String finalizeBlob() {
178+
if (mBlockingErrors.isEmpty()) {
179+
return "";
180+
}
181+
182+
try {
183+
JSONObject blob = new JSONObject();
184+
185+
// Seed fields
186+
blob.put(FIELD_SCHEMA_VERSION, mSchemaVersion);
187+
blob.put(FIELD_SESSION_CORRELATION_ID, mSessionCorrelationId);
188+
blob.put(FIELD_ONBOARDING_MODE, mOnboardingMode);
189+
190+
// StepsList
191+
JSONArray steps = new JSONArray();
192+
for (StepEntry entry : mStepsList) {
193+
JSONObject step = new JSONObject();
194+
step.put(FIELD_STEP_ID, entry.stepId);
195+
step.put(FIELD_TS, entry.timestamp);
196+
steps.put(step);
197+
}
198+
blob.put(FIELD_STEPS_LIST, steps);
199+
200+
// Platform builder fields
201+
JSONArray errorsArray = new JSONArray();
202+
for (String error : mBlockingErrors) {
203+
errorsArray.put(error);
204+
}
205+
blob.put(OnboardingBlobFieldKeys.BLOCKING_ERRORS, errorsArray);
206+
blob.put(OnboardingBlobFieldKeys.LAST_BLOCKING_ERROR,
207+
mBlockingErrors.get(mBlockingErrors.size() - 1));
208+
blob.put(OnboardingBlobFieldKeys.REMEDIATION_NEEDED, mRemediationNeeded);
209+
210+
if (mLastLoadedDomain != null) {
211+
blob.put(OnboardingBlobFieldKeys.LAST_LOADED_DOMAIN, mLastLoadedDomain);
212+
}
213+
214+
if (!mStepsList.isEmpty()) {
215+
blob.put(OnboardingBlobFieldKeys.LAST_COMPLETED_STEP,
216+
mStepsList.get(mStepsList.size() - 1).stepId);
217+
}
218+
219+
if (mProfile != null) {
220+
blob.put(OnboardingBlobFieldKeys.PROFILE, mProfile);
221+
}
222+
223+
if (!mUxFlowUsed.isEmpty()) {
224+
JSONArray flows = new JSONArray();
225+
for (String flow : mUxFlowUsed) {
226+
flows.put(flow);
227+
}
228+
blob.put(OnboardingBlobFieldKeys.UX_FLOW_USED, flows);
229+
}
230+
231+
return blob.toString();
232+
} catch (JSONException e) {
233+
return "";
234+
}
235+
}
236+
237+
/**
238+
* Returns the session correlation ID from the seed blob.
239+
*/
240+
@NonNull
241+
public String getSessionCorrelationId() {
242+
return mSessionCorrelationId;
243+
}
244+
245+
/**
246+
* Persist session correlation entry to SharedPreferences immediately.
247+
* Called on block detection for app-kill resilience.
248+
*/
249+
private void persistSessionCorrelation() {
250+
try {
251+
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
252+
String existing = prefs.getString(PREFS_FILE, "");
253+
JSONObject cache;
254+
if (existing != null && !existing.isEmpty()) {
255+
cache = new JSONObject(existing);
256+
} else {
257+
cache = new JSONObject();
258+
}
259+
260+
String key = mClientId + "|" + mTarget;
261+
JSONObject entry = new JSONObject();
262+
entry.put("id", mSessionCorrelationId);
263+
entry.put("ts", System.currentTimeMillis());
264+
cache.put(key, entry);
265+
266+
prefs.edit().putString(PREFS_FILE, cache.toString()).apply();
267+
} catch (JSONException e) {
268+
// Best-effort persistence — don't crash
269+
}
270+
}
271+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.identity.common.internal.telemetry;
5+
6+
import android.content.Context;
7+
import android.content.SharedPreferences;
8+
9+
import androidx.annotation.NonNull;
10+
11+
/**
12+
* SharedPreferences-backed persistence for session correlation IDs.
13+
* Used by both OneAuth (via JNI/Djinni) and the broker app directly.
14+
* The same SharedPreferences file is written to by OnboardingBlobBuilder
15+
* on block detection for app-kill resilience.
16+
*/
17+
public class OnboardingSessionCachePersistence {
18+
19+
private static final String PREFS_FILE = "com.microsoft.oneauth.session_correlation_cache";
20+
21+
private final Context mContext;
22+
23+
public OnboardingSessionCachePersistence(@NonNull Context context) {
24+
mContext = context.getApplicationContext();
25+
}
26+
27+
/**
28+
* Load the persisted session correlation cache JSON string.
29+
* @return JSON string, or empty string if nothing is persisted
30+
*/
31+
@NonNull
32+
public String load() {
33+
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
34+
String value = prefs.getString(PREFS_FILE, "");
35+
return value != null ? value : "";
36+
}
37+
38+
/**
39+
* Save the session correlation cache JSON string to SharedPreferences.
40+
* @param json The JSON string to persist
41+
*/
42+
public void save(@NonNull String json) {
43+
SharedPreferences prefs = mContext.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
44+
prefs.edit().putString(PREFS_FILE, json).apply();
45+
}
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.identity.common.java.telemetry;
5+
6+
/**
7+
* JSON field keys owned by platform builders. C++ does NOT use these by name —
8+
* EntityStore dynamically iterates the blob JSON for fan-out.
9+
* Seed creation + aggregation keys come from OnboardingBlobConstants (Djinni-generated).
10+
*/
11+
public final class OnboardingBlobFieldKeys {
12+
// Field keys for populated blob (written by builder, read by EntityStore dynamically)
13+
public static final String BLOCKING_ERRORS = "blockingErrors";
14+
public static final String LAST_BLOCKING_ERROR = "lastBlockingError";
15+
public static final String LAST_LOADED_DOMAIN = "lastLoadedDomain";
16+
public static final String LAST_COMPLETED_STEP = "lastCompletedStep";
17+
public static final String REMEDIATION_NEEDED = "remediationNeeded";
18+
public static final String PROFILE = "profile";
19+
public static final String UX_FLOW_USED = "uxFlowUsed";
20+
21+
// Step ID values not used in C++ aggregation (no derived duration metric computed from these)
22+
public static final String STEP_AUTHENTICATION_STARTED = "AuthenticationStarted";
23+
public static final String STEP_CREDENTIAL_ENTRY_COMPLETED = "CredentialEntryCompleted";
24+
public static final String STEP_BROKER_INSTALL_PROMPTED = "BrokerInstallPrompted";
25+
public static final String STEP_BROKER_INSTALL_PROMPTED_FOR_MDM = "BrokerInstallPromptedForMDM";
26+
public static final String STEP_DEVICE_REGISTRATION_STARTED = "DeviceRegistrationStarted";
27+
public static final String STEP_DEVICE_REGISTRATION_COMPLETED = "DeviceRegistrationCompleted";
28+
public static final String STEP_FLOW_COMPLETED = "FlowCompleted";
29+
30+
// Platform-specific values
31+
public static final String PROFILE_USER = "userProfile";
32+
public static final String PROFILE_WORK = "workProfile";
33+
34+
private OnboardingBlobFieldKeys() {} // non-instantiable
35+
}

0 commit comments

Comments
 (0)