Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7523205
added by pass consent public api
namArora3112 Sep 30, 2026
950b0ed
added dedepulication logic along with public api map arguments
namArora3112 Oct 6, 2026
628ad66
added unit tests for identity extension and device deduplicate logic
namArora3112 Oct 6, 2026
29531a4
persist consent-aware device attribute sync
namArora3112 Oct 13, 2026
cc3ef46
parity with ios names and variables
namArora3112 Oct 14, 2026
28e22a8
added tests device attribute
namArora3112 Oct 15, 2026
206104c
Added dedup flag name change
namArora3112 Oct 15, 2026
9e2ff4e
refactored dedup classes along with tests
namArora3112 Oct 15, 2026
1afa674
push token payload fixes
namArora3112 Oct 15, 2026
eeebe16
Removed value key payload
namArora3112 Oct 16, 2026
09d301f
Apply device attribute sync review fixes
sagar-sharma-adobe Oct 16, 2026
9418ad9
Bring Edge Identity device attribute sync into iOS/Android parity
sagar-sharma-adobe Oct 17, 2026
9ed9524
Report results for device attribute requests that are not sent to Edge
sagar-sharma-adobe Oct 17, 2026
3a537d5
Notify Messaging when device attribute tokens are confirmed
sagar-sharma-adobe Oct 17, 2026
86273e4
Harden device attribute sync against unexpected errors
sagar-sharma-adobe Oct 17, 2026
79c892a
Deduplicate only known device attributes
sagar-sharma-adobe Oct 17, 2026
cf605c0
Clarify device attributes comments
sagar-sharma-adobe Oct 19, 2026
220915e
Align device attribute deduplication with iOS
namArora3112 Oct 19, 2026
0dd8609
Fix Checkstyle violations in device attribute deduplicator
namArora3112 Oct 19, 2026
1528536
Clarify device attributes comments
sagar-sharma-adobe Oct 19, 2026
cc10f78
Rename device attributes API to operational data
sagar-sharma-adobe Oct 21, 2026
9ff47de
Match operational data events by type and source
sagar-sharma-adobe Oct 21, 2026
3bb9d2a
Update Core to 3.11.0
sagar-sharma-adobe Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Documentation/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ Refer to the [Getting Started Guide](getting-started.md)
| [resetIdentities](#resetIdentities) |
| [setAdvertisingIdentifier](#setAdvertisingIdentifier) |
| [updateIdentities](#updateIdentities) |
| [updateOperationalData](#updateoperationaldata) |

------

### updateOperationalData

Requests that operational data allowed by a matching rule is sent to the Edge Network. It supports JSON-compatible values, including empty strings, arrays, and objects. Values are not validated by Identity and are sent as provided. Only string values at `timezone` and `tokens.pushNotification` are deduplicated and stored; unexpected types at those paths and all other attributes are always forwarded. This API is fire-and-forget; it has no public callback.

```java
public static void updateOperationalData(Map<String, Object> attributes)
```

```java
Identity.updateOperationalData(
Collections.singletonMap("timezone", "Asia/Tokyo"));
```

The data is sent to the Core rules engine. Each matching `forward-operational-data` rule returns only its enabled paths. Values a rule allows are sent in a device attributes request event whether collect consent is `yes`, `no`, or `pending`; ordinary Edge events remain governed by consent. When the latest collect consent is explicitly `y`, or the Hub shared state confirms that the Consent extension is not registered, Identity also compares push tokens with Messaging's latest shared-state value and timezone with the timezone set through the profile attributes API. Missing, pending, or malformed consent from a registered Consent extension does not enable these comparisons. Values Edge confirmed are compared at every consent state. Identity queues requests in event order and reads the current configuration, profile timezone and Messaging push token when each request is taken from the queue. It waits for the active request's device attributes response event before sending the next request. A response that isn't a success, including one with no status, is reported as failed; responses for other requests are ignored. Confirmed values are persisted and deduplicated across app restarts; set `edgeidentity.optimizeOperationalProfileDataSync` to `false` to disable all comparisons, but requests still run one at a time. If a request fails, its values are not stored and a queued duplicate can send them again. If no rule matches, or rules are not loaded yet, nothing is sent.

After Edge reports success, confirmed strings are persisted under the individual `timezone` and `pushNotification` keys in the separate `operationalDataDataStore` collection. A confirmed timezone also replaces the timezone set through the profile attributes API. The complete confirmed state remains published under `profileAttributes` in Identity's standard shared state for the existing Android Messaging consumer. On registration, this shared state is published only when stored attributes exist; explicit updates and identity reset still publish an empty state when appropriate. In addition, Identity dispatches an `Operational data confirmed` event (`messaging` / `responseContent`), with the response event as its parent, containing only the successfully sent `tokens`, matching iOS. Failed, dropped, malformed, and unrelated results do not confirm tokens.

An empty map is a no-op. Identity does not drop null values during deduplication, although the rules engine and transport determine which values can reach Edge. After rules and deduplication filter the values, every dispatched request includes SDK-owned `app.id` (the Android package name) and `app.platform: "fcm"`; caller-provided `app` values cannot override routing, and app-only requests are not sent. Live Activity deduplication and APNs sandbox routing are iOS-specific; arbitrary arrays allowed by a rule remain supported on Android.

------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,48 @@ public static void updateIdentities(@NonNull final IdentityMap identityMap) {
MobileCore.dispatchEvent(updateIdentitiesEvent);
}

/**
* Updates JSON-compatible operational data that should be delivered to the Edge Network
* independently of collect consent. Launch rules decide which values are sent. By default, the timezone and
* push token are sent only when they differ from the last value Edge confirmed. When collect consent is "y", or
* the Consent extension is not registered, they are also not sent when they match the timezone set through the
* profile attributes API or Messaging's push token. All other values are always sent; set
* {@code edgeidentity.optimizeOperationalProfileDataSync} to {@code false} in Configuration to send every update.
* Requests are sent one at a time. Values are not validated by this API and are sent as
* provided. Only string timezone and push-token values are deduplicated. Any {@code app} value is replaced by
* the SDK.
*
* @param attributes map of attribute names to JSON-compatible values
*/
public static void updateOperationalData(@NonNull final Map<String, Object> attributes) {
try {
if (attributes == null || attributes.isEmpty()) {
Log.warning(
IdentityConstants.LOG_TAG,
LOG_SOURCE,
"Unable to update operational data, attributes are null or empty."
);
return;
}
final Event event = new Event.Builder(
IdentityConstants.EventNames.UPDATE_OPERATIONAL_DATA,
EventType.EDGE_IDENTITY,
IdentityConstants.OperationalData.REQUEST_SOURCE
)
.setEventData(new HashMap<>(attributes))
.build();
MobileCore.dispatchEvent(event);
} catch (Exception exception) {
// The caller's map may be changed on another thread while it is copied; never throw into the app.
Log.warning(
IdentityConstants.LOG_TAG,
LOG_SOURCE,
"Unable to update operational data, an error occurred: %s",
exception.getLocalizedMessage()
);
}
}

/**
* Removes the identity from the stored client-side {@link IdentityMap}. The Identity extension will stop sending this identifier.
* This does not clear the identifier from the User Profile Graph.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ static final class EventNames {
static final String REQUEST_IDENTITIES = "Edge Identity Request Identities";
static final String RESET_IDENTITIES_COMPLETE = "Edge Identity Reset Identities Complete";
static final String UPDATE_PROFILE_ATTRIBUTES = "Update Profile Attributes";
static final String UPDATE_OPERATIONAL_DATA = "Update Operational Data";
static final String OPERATIONAL_DATA_TO_RULES_ENGINE = "Operational data for rules engine";
static final String EDGE_DEVICE_ATTRIBUTES_REQUEST = "Edge device attributes request";
static final String OPERATIONAL_DATA_SYNC_RESULT = "Operational Data Sync Result";
static final String OPERATIONAL_DATA_CONFIRMED = "Operational data confirmed";

private EventNames() {}
}
Expand Down Expand Up @@ -68,6 +73,25 @@ static final class Configuration {
private Configuration() {}
}

static final class Consent {

static final String NAME = "com.adobe.edge.consent";
static final String CONSENTS = "consents";
static final String COLLECT = "collect";
static final String VALUE = "val";
static final String YES = "y";

private Consent() {}
}

static final class Messaging {

static final String NAME = "com.adobe.messaging";
static final String PUSH_IDENTIFIER = "pushidentifier";

private Messaging() {}
}

static final class IdentityDirect {

static final String NAME = "com.adobe.module.identity";
Expand Down Expand Up @@ -118,6 +142,7 @@ static final class DataStoreKey {
static final String IDENTITY_DIRECT_DATASTORE_NAME = "visitorIDServiceDataStore";
static final String IDENTITY_DIRECT_ECID_KEY = "ADOBEMOBILE_PERSISTED_MID";
static final String PROFILE_ATTRIBUTES_DATASTORE_NAME = "profileAttributesDataStore";
static final String OPERATIONAL_DATA_DATASTORE_NAME = "operationalDataDataStore";

private DataStoreKey() {}
}
Expand All @@ -138,6 +163,9 @@ private UrlKeys() {}
*/
static final class ProfileAttributes {

// Key of the profile attributes in the Edge Identity standard (non-XDM) shared state.
static final String SHARED_STATE_KEY = "profileAttributes";

// Keys for the outgoing generic EDGE request event payload. The event carries the collated
// data from every profile-attribute helper under a single "profile.updateAttributes" type.
// Per-attribute event-data keys, payload keys, and persistence keys are owned by the
Expand All @@ -150,5 +178,35 @@ static final class ProfileAttributes {
private ProfileAttributes() {}
}

static final class OperationalData {

static final String REQUEST_SOURCE = "com.adobe.eventSource.requestOperationalData";
static final String RULE_RESPONSE_SOURCE = "com.adobe.eventSource.responseOperationalData";
static final String EDGE_DEVICE_ATTRIBUTES_TYPE = "com.adobe.eventType.edgeDeviceAttributes";
static final String OPERATIONAL_DATA_TYPE = "com.adobe.eventType.generic.operationalData";
static final String OPTIMIZE_OPERATIONAL_PROFILE_DATA_SYNC = "edgeidentity.optimizeOperationalProfileDataSync";
static final String TIME_ZONE = "timeZone";
static final String TIMEZONE = "timezone";
static final String PUSH_NOTIFICATION = "pushNotification";
static final String TOKENS = "tokens";
static final String APP = "app";
static final String APP_ID = "id";
static final String APP_PLATFORM = "platform";
static final String PLATFORM_FCM = "fcm";
static final String STATUS = "status";
static final String STATUS_SUCCESS = "success";
static final String STATUS_FAILED = "failed";
static final String REASON = "reason";
static final String REQUEST_EVENT_ID = "requestEventId";
static final String ATTRIBUTES = "attributes";
static final String UNKNOWN_REASON = "unknown";
static final String REASON_UNCHANGED = "unchanged";
static final String REASON_IDENTITY_RESET = "identityReset";
static final String REASON_MISSING_APP_IDENTIFIER = "missingAppIdentifier";
static final String REASON_INTERNAL_ERROR = "internalError";

private OperationalData() {}
}

private IdentityConstants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,15 @@ public SharedStateResult getSharedState(final String stateOwner, final Event eve
public void createXDMSharedState(final Map<String, Object> state, final Event event) {
getApi().createXDMSharedState(state, event);
}

@Override
public void publishProfileAttributes(final Event event) {
operationalDataSync.publishProfileAttributes(event);
}
};

private final IdentityState state;
private final OperationalDataSync operationalDataSync;

/**
* Constructor.
Expand All @@ -64,6 +70,7 @@ protected IdentityExtension(final ExtensionApi extensionApi) {
IdentityExtension(final ExtensionApi extensionApi, final IdentityState state) {
super(extensionApi);
this.state = state;
this.operationalDataSync = new OperationalDataSync(extensionApi, state.getProfileAttributeStore());
}

@NonNull @Override
Expand Down Expand Up @@ -94,6 +101,9 @@ protected String getVersion() {
* <li> EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#UPDATE_IDENTITY}</li>
* <li> EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link EventSource#REMOVE_IDENTITY}</li>
* <li> EventType {@link EventType#HUB} and EventSource {@link EventSource#SHARED_STATE}</li>
* <li> EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.OperationalData#REQUEST_SOURCE}</li>
* <li> EventType {@link EventType#EDGE_IDENTITY} and EventSource {@link IdentityConstants.OperationalData#RULE_RESPONSE_SOURCE}</li>
* <li> EventType {@link IdentityConstants.OperationalData#EDGE_DEVICE_ATTRIBUTES_TYPE} and EventSource {@link EventSource#RESPONSE_CONTENT}</li>
* </ul>
* </p>
*/
Expand Down Expand Up @@ -127,6 +137,27 @@ protected void onRegistered() {

// HUB shared state event listener
getApi().registerEventListener(EventType.HUB, EventSource.SHARED_STATE, this::handleIdentityDirectECIDUpdate);

// Operational data sync listeners
getApi()
.registerEventListener(
EventType.EDGE_IDENTITY,
IdentityConstants.OperationalData.REQUEST_SOURCE,
operationalDataSync::handleOperationalData
);
getApi()
.registerEventListener(
EventType.EDGE_IDENTITY,
IdentityConstants.OperationalData.RULE_RESPONSE_SOURCE,
operationalDataSync::handleOperationalDataRuleResponse
);
getApi()
.registerEventListener(
IdentityConstants.OperationalData.EDGE_DEVICE_ATTRIBUTES_TYPE,
EventSource.RESPONSE_CONTENT,
operationalDataSync::handleOperationalDataResult
);
operationalDataSync.publishStoredProfileAttributes();
}

@Override
Expand Down Expand Up @@ -344,9 +375,20 @@ void handleRequestReset(@NonNull final Event event) {
// Add pending shared state to avoid race condition between updating and reading identity map
final SharedStateResolver resolver = getApi().createPendingXDMSharedState(event);
state.resetIdentifiers();
// Clear synced profile attributes (and update their shared state) so the next update re-syncs
// against the new ECID
state.clearProfileAttributes(sharedStateHandle, event);
// Clear the stored profile attributes and operational data (and update their shared states) so the next update is sent
// again for the new ECID
try {
state.clearProfileAttributes(sharedStateHandle, event);
operationalDataSync.reset(event);
} catch (Exception exception) {
// The pending XDM shared state must always be resolved, or extensions reading it would wait forever.
Log.warning(
IdentityConstants.LOG_TAG,
LOG_SOURCE,
"Unable to clear profile attributes and operational data during identity reset: %s",
exception.getLocalizedMessage()
);
}
resolver.resolve(state.getIdentityProperties().toXDMData());

// dispatch reset complete event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class IdentityState {
return identityProperties;
}

ProfileAttributeStore getProfileAttributeStore() {
return profileAttributeStore;
}

/**
* Completes init for this Identity extension.
* If no ECID is loaded from persistence (ideally meaning first launch), attempts to migrate existing ECID
Expand Down Expand Up @@ -370,7 +374,7 @@ private void dispatchAdIdConsentRequestEvent(final String consentVal) {
void updateProfileAttributes(final Event event, final SharedStateCallback callback) {
final Map<String, Object> eventData = event.getEventData();
if (MapUtils.isNullOrEmpty(eventData)) {
Log.warning(
Log.debug(
IdentityConstants.LOG_TAG,
LOG_SOURCE,
"Event data is null or empty for '" + event.getName() + "'; skipping update."
Expand All @@ -390,16 +394,17 @@ void updateProfileAttributes(final Event event, final SharedStateCallback callba
}

if (mergedAttributes.isEmpty()) {
Log.warning(
Log.debug(
IdentityConstants.LOG_TAG,
LOG_SOURCE,
"No profile attributes collected from event data; skipping update."
);
return;
}

// Persistence just changed: keep the shared state in sync, then dispatch only the changes.
// Persistence just changed: keep the shared states in sync, then dispatch only the changes.
publishProfileAttributesSharedState(callback, event);
callback.publishProfileAttributes(event);
dispatchProfileAttributesEdgeEvent(mergedAttributes);
}

Expand Down
Loading
Loading