Skip to content

feat: Migrate setUserAttribute#689

Merged
denischilik merged 10 commits intoworkstation/6.0-Releasefrom
feat/migrate-set-user-attribute
Mar 31, 2026
Merged

feat: Migrate setUserAttribute#689
denischilik merged 10 commits intoworkstation/6.0-Releasefrom
feat/migrate-set-user-attribute

Conversation

@denischilik
Copy link
Copy Markdown

@denischilik denischilik commented Mar 31, 2026

Background

Kits that implement UserAttributeListener should handle onSetUserAttribute with the same null-safe contract as other kits and forward string values to existing setUserAttribute logic where appropriate.

What Has Changed

  • SingularKit: onSetUserAttribute uses key / value / user naming, nullable parameters, and guards before calling setUserAttribute.
  • AppsFlyerKit & Braze (38–41): aligned UserAttributeListener overrides for consistent Kotlin signatures and null checks.

Screenshots/Video

N/A

Checklist

  • I have performed a self-review of my own code.
  • I have made corresponding changes to the documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have tested this locally.

Additional Notes

Single commit on top of workstation/6.0-Release.

…arams, naming)

- SingularKit: key/value/user naming, null checks for Java interop
- AppsFlyerKit, Braze AppboyKit: consistent UserAttributeListener overrides

Made-with: Cursor
@denischilik denischilik requested a review from a team as a code owner March 31, 2026 14:53
@cursor
Copy link
Copy Markdown

cursor bot commented Mar 31, 2026

PR Summary

Medium Risk
Touches core attribute-forwarding logic in KitManagerImpl and updates many kits to a new callback signature, so regressions could drop or mis-handle user attributes. Most changes are mechanical with added null/type guards and updated tests.

Overview
This PR migrates scalar user-attribute delivery to a single BaseAttributeListener.onSetUserAttribute(key, value, user) callback, removing AttributeListener.setUserAttribute and updating KitManagerImpl to invoke the new hook for both scalar attributes and list-to-scalar fallbacks.

Kits are updated to implement the new onSetUserAttribute contract (nullable args + type checks) and route string values into existing behavior where needed (e.g., Adobe kits syncing IDs, Apptimize/Comscore/Localytics applying scalar attributes, UrbanAirship tag mapping, Singular’s age/gender mapping, and no-op kits adding stubs). Tests and test kits are adjusted accordingly, including renamed callbacks and updated Mockito expectations.

Written by Cursor Bugbot for commit 50415b5. This will update automatically on new commits. Configure here.

- KitIntegration: declare onSetUserAttribute on BaseAttributeListener; remove from UserAttributeListener
- KitManagerImpl: dispatch scalar and CSV list paths via BaseAttributeListener; avoid double calls for dual-interface kits
- AttributeListener-only kits: delegate onSetUserAttribute to setUserAttribute
- AttributeListenerTestKit: implement onSetUserAttribute

Made-with: Cursor
…tribute

- Drop setUserAttribute from KitIntegration.AttributeListener; kits keep
  setUserAttribute as a regular method (no override).
- KitManagerImpl: use BaseAttributeListener.onSetUserAttribute for scalar and
  joined list paths; prefer UserAttributeListener when both interfaces apply.
- AttributeListenerTestKit: rename callback to setUserAttributeCallback;
  update DataplanBlockingUserTests.

Made-with: Cursor
if (provider instanceof KitIntegration.AttributeListener) {
((KitIntegration.AttributeListener) provider).setUserAttribute(key, newValue);
if (provider instanceof KitIntegration.BaseAttributeListener listener) {
listener.onSetUserAttribute(key, newValue, FilteredMParticleUser.getInstance(mpid, provider));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

onSetUserAttribute bypasses disabled and filter guards in increment

High Severity

In incrementUserAttribute, the onSetUserAttribute call on BaseAttributeListener at line 743 sits outside the braceless if (!provider.isDisabled() && shouldForwardAttribute(...)) guard at line 739. That guard only controls the nested onIncrementUserAttribute call. As a result, onSetUserAttribute is dispatched to every BaseAttributeListener kit regardless of whether the provider is disabled or the attribute is filtered. Compare this to the properly guarded setUserAttribute(provider, key, value, mpid) private helper and removeUserAttribute, which both wrap their calls inside the full check. Additionally, the old code scoped this unguarded path to AttributeListener only, but the change to BaseAttributeListener now also dispatches to all UserAttributeListener kits (e.g. Braze, Singular), which never received this call during increments before.

Additional Locations (1)
Fix in Cursor Fix in Web

…serAttribute

- Move scalar attribute logic from removed setUserAttribute helpers into
  onSetUserAttribute (or private helpers shared with setAllUserAttributes).
- Braze: applyScalarUserAttribute for onSet + initial sync; update unit tests
  to call onSetUserAttribute with a mocked FilteredMParticleUser.
- AttributeListenerTestKit: inline callback/onAttributeReceived in
  onSetUserAttribute.

Made-with: Cursor
…r for list attrs

Re-run both branches when a kit implements both interfaces; UserAttributeListener
join path calls UserAttributeListener.onSetUserAttribute again.

Made-with: Cursor
Split applyScalarUserAttribute into applyScalarUserAttributeOnUser plus
focused helpers for age, email/push subscribe, gender, subscription groups,
and unmapped custom keys (braze-38 through braze-41).

Made-with: Cursor
AttributeListener no longer has setUserAttribute; assert BaseAttributeListener
onSetUserAttribute with joined value and FilteredMParticleUser.

Made-with: Cursor
FilteredMParticleUser.getInstance returns null in JVM unit tests; Mockito
any(Class) does not match null. Use isNull() for the third onSetUserAttribute arg.

Made-with: Cursor
Copy link
Copy Markdown

@nickolas-dimitrakas nickolas-dimitrakas left a comment

Choose a reason for hiding this comment

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

LGTM, walkthrough live

s: String,
o: Any,
filteredMParticleUser: FilteredMParticleUser,
key: String?,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is this key nullable?

- Use String? and Any? for parameters matching Java Object/null call paths
- Add early returns when key or value is null where needed
- CleverTap: rename parameters to key/value; use mappedKey/mappedValue for mutations

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

user: FilteredMParticleUser,
) {
if (key == null) {
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing null guard for value in LeanplumKit

Low Severity

The onSetUserAttribute override widens value from Any (non-null) to Any? (nullable), but the null guard on line 221 only checks key == null, not value == null. Every other kit in this PR guards against null value, yet here a null value is silently forwarded into setAttributesAndCheckId. The old non-nullable value: Any parameter prevented this; the new nullable value: Any? parameter without a corresponding guard is inconsistent with the PR's stated null-safe contract.

Fix in Cursor Fix in Web

Third parameter is user: FilteredMParticleUser? to match KitManagerImpl and
FilteredMParticleUser.getInstance. GA/GA4 use parameter name user.

Made-with: Cursor
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
69.9% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@denischilik denischilik merged commit e5f50e4 into workstation/6.0-Release Mar 31, 2026
21 of 23 checks passed
@denischilik denischilik deleted the feat/migrate-set-user-attribute branch March 31, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants