diff --git a/android-core/src/main/java/com/mparticle/MParticle.java b/android-core/src/main/java/com/mparticle/MParticle.java index 76cf029ed..44c61a0ea 100644 --- a/android-core/src/main/java/com/mparticle/MParticle.java +++ b/android-core/src/main/java/com/mparticle/MParticle.java @@ -20,6 +20,9 @@ import androidx.annotation.RequiresApi; import com.mparticle.commerce.CommerceEvent; +import com.mparticle.commerce.Impression; +import com.mparticle.commerce.Product; +import com.mparticle.commerce.Promotion; import com.mparticle.consent.ConsentState; import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; @@ -60,6 +63,7 @@ import java.math.BigDecimal; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -148,9 +152,10 @@ private MParticle(MParticleOptions options) { * @param interval in seconds */ public void setUpdateInterval(int interval) { + MParticle.logRoktApiUsage("SET_UPLOAD_INTERVAL"); long intervalMillis = interval * 1000L; if ((intervalMillis >= 1 && mConfigManager.getUploadInterval() != intervalMillis)) { - upload(); + withoutRoktApiUsage(this::upload); mConfigManager.setUploadInterval(interval); } } @@ -281,6 +286,45 @@ public static void setInstance(@Nullable MParticle instance) { MParticle.instance = instance; } + // Rokt public-API-usage diagnostics are suppressed while SDK/kit internals invoke a public API + // (auto-upload, the Rokt kit's attribute enrichment, deferred push-token modify, etc.) so only + // genuine partner calls are reported. Synchronous by design: logRoktApiUsage runs at each + // instrumented method's first line, on the caller's thread, before any dispatch — so this flag + // is active for that call. ponytail: covers synchronous re-entry only, which is exactly how the + // instrumented methods emit; async internal paths that need it call withoutRoktApiUsage directly. + private static final ThreadLocal sSuppressRoktApiUsage = new ThreadLocal<>(); + + /** + * @hide Internal: forwards a bounded, non-PII public-API-usage diagnostic code to the Rokt kit + * (only when the kit is active). No-op when mParticle isn't started, Rokt isn't integrated, or + * the call originates from SDK/kit internals (see {@link #withoutRoktApiUsage(Runnable)}). + * Reads the static instance directly so it stays quiet (no getInstance() warning) on the hot path. + * Not intended for partner use. + */ + public static void logRoktApiUsage(@Nullable String code) { + if (Boolean.TRUE.equals(sSuppressRoktApiUsage.get())) { + return; + } + MParticle mp = instance; + if (mp != null && mp.mKitManager != null) { + mp.mKitManager.logRoktApiDiagnostic(code); + } + } + + /** + * @hide Internal: run SDK/kit work that invokes public APIs without emitting Rokt usage + * diagnostics, so internal re-entry isn't misreported as a partner call. Not for partner use. + */ + public static void withoutRoktApiUsage(@NonNull Runnable action) { + boolean previous = Boolean.TRUE.equals(sSuppressRoktApiUsage.get()); + sSuppressRoktApiUsage.set(true); + try { + action.run(); + } finally { + sSuppressRoktApiUsage.set(previous); + } + } + /** * Switch the SDK to a new API key and secret. * Will first batch all events that have not been sent to mParticle into upload records, @@ -291,6 +335,7 @@ public static void setInstance(@Nullable MParticle instance) { @param options Required to initialize the SDK properly */ public static void switchWorkspace(@NonNull MParticleOptions options) { + MParticle.logRoktApiUsage("SWITCH_WORKSPACE"); synchronized (MParticle.class) { MParticle localInstance = instance; if (localInstance == null) { @@ -446,6 +491,7 @@ boolean isSessionActive() { * Force upload all queued messages to the mParticle server. */ public void upload() { + MParticle.logRoktApiUsage("UPLOAD"); mMessageManager.doUpload(); } @@ -454,6 +500,7 @@ public void upload() { * automatically retrieved upon installation from Google Play. */ public void setInstallReferrer(@Nullable String referrer) { + MParticle.logRoktApiUsage("SET_INSTALL_REFERRER"); InstallReferrerHelper.setInstallReferrer(mAppContext, referrer); } @@ -468,6 +515,7 @@ public String getInstallReferrer() { } public void logEvent(@NonNull BaseEvent event) { + MParticle.logRoktApiUsage(getRoktLogEventDiagnosticCode(event)); if (event instanceof MPEvent && event.isShouldUploadEvent()) { logMPEvent((MPEvent) event); } else if (event instanceof CommerceEvent && event.isShouldUploadEvent()) { @@ -481,6 +529,86 @@ public void logEvent(@NonNull BaseEvent event) { } } + @NonNull + private static String getRoktLogEventDiagnosticCode(@Nullable BaseEvent event) { + if (event instanceof MPEvent) { + EventType eventType = ((MPEvent) event).getEventType(); + if (eventType == null) { + return "LOG_EVENT_UNKNOWN"; + } + switch (eventType) { + case Navigation: + return "LOG_EVENT_NAVIGATION"; + case Location: + return "LOG_EVENT_LOCATION"; + case Search: + return "LOG_EVENT_SEARCH"; + case Transaction: + return "LOG_EVENT_TRANSACTION"; + case UserContent: + return "LOG_EVENT_USER_CONTENT"; + case UserPreference: + return "LOG_EVENT_USER_PREFERENCE"; + case Social: + return "LOG_EVENT_SOCIAL"; + case Other: + return "LOG_EVENT_OTHER"; + case Media: + return "LOG_EVENT_MEDIA"; + case Unknown: + default: + return "LOG_EVENT_UNKNOWN"; + } + } + if (event instanceof CommerceEvent) { + return getRoktCommerceEventDiagnosticCode((CommerceEvent) event); + } + return "LOG_EVENT_OTHER"; + } + + @NonNull + private static String getRoktCommerceEventDiagnosticCode(@NonNull CommerceEvent event) { + String productAction = event.getProductAction(); + if (Product.ADD_TO_CART.equals(productAction)) { + return "LOG_EVENT_PRODUCT_ADD_TO_CART"; + } else if (Product.REMOVE_FROM_CART.equals(productAction)) { + return "LOG_EVENT_PRODUCT_REMOVE_FROM_CART"; + } else if (Product.ADD_TO_WISHLIST.equals(productAction)) { + return "LOG_EVENT_PRODUCT_ADD_TO_WISHLIST"; + } else if (Product.REMOVE_FROM_WISHLIST.equals(productAction)) { + return "LOG_EVENT_PRODUCT_REMOVE_FROM_WISHLIST"; + } else if (Product.CHECKOUT.equals(productAction)) { + return "LOG_EVENT_PRODUCT_CHECKOUT"; + } else if (Product.CHECKOUT_OPTION.equals(productAction)) { + return "LOG_EVENT_PRODUCT_CHECKOUT_OPTION"; + } else if (Product.CLICK.equals(productAction)) { + return "LOG_EVENT_PRODUCT_CLICK"; + } else if (Product.DETAIL.equals(productAction)) { + return "LOG_EVENT_PRODUCT_VIEW_DETAIL"; + } else if (Product.PURCHASE.equals(productAction)) { + return "LOG_EVENT_PRODUCT_PURCHASE"; + } else if (Product.REFUND.equals(productAction)) { + return "LOG_EVENT_PRODUCT_REFUND"; + } else if (productAction != null) { + return "LOG_EVENT_COMMERCE_OTHER"; + } + + String promotionAction = event.getPromotionAction(); + if (Promotion.VIEW.equals(promotionAction)) { + return "LOG_EVENT_PROMOTION_VIEW"; + } else if (Promotion.CLICK.equals(promotionAction)) { + return "LOG_EVENT_PROMOTION_CLICK"; + } else if (promotionAction != null) { + return "LOG_EVENT_COMMERCE_OTHER"; + } + + List impressions = event.getImpressions(); + if (impressions != null && !impressions.isEmpty()) { + return "LOG_EVENT_PRODUCT_IMPRESSION"; + } + return "LOG_EVENT_COMMERCE_OTHER"; + } + /** * Log an event with an {@link MPEvent} object. * @@ -519,6 +647,7 @@ private void logCommerceEvent(@NonNull CommerceEvent event) { * @param contextInfo An MPProduct or any set of data to associate with this increase in LTV (optional) */ public void logLtvIncrease(@NonNull BigDecimal valueIncreased, @Nullable String eventName, @Nullable Map contextInfo) { + MParticle.logRoktApiUsage("LOG_LTV_INCREASE"); if (valueIncreased == null) { Logger.error("ValueIncreased must not be null."); return; @@ -528,11 +657,13 @@ public void logLtvIncrease(@NonNull BigDecimal valueIncreased, @Nullable String } contextInfo.put(MessageKey.RESERVED_KEY_LTV, valueIncreased.toPlainString()); contextInfo.put(Constants.MethodName.METHOD_NAME, Constants.MethodName.LOG_LTV); - logEvent( + final Map ltvContextInfo = contextInfo; + // Internal re-entry: this is LOG_LTV_INCREASE, not a partner LOG_EVENT call. + withoutRoktApiUsage(() -> logEvent( new MPEvent.Builder(eventName == null ? "Increase LTV" : eventName, EventType.Transaction) - .customAttributes(contextInfo) + .customAttributes(ltvContextInfo) .build() - ); + )); } /** @@ -572,6 +703,7 @@ public void logScreen(@NonNull String screenName, @Nullable Map * @param screenEvent an event object, the name of the event will be used as the screen name */ public void logScreen(@NonNull MPEvent screenEvent) { + MParticle.logRoktApiUsage("LOG_SCREEN"); screenEvent.setScreenEvent(true); if (MPUtility.isEmpty(screenEvent.getEventName())) { Logger.error("screenName is required for logScreen."); @@ -598,6 +730,7 @@ public void logScreen(@NonNull MPEvent screenEvent) { * @param breadcrumb */ public void leaveBreadcrumb(@NonNull String breadcrumb) { + MParticle.logRoktApiUsage("LEAVE_BREADCRUMB"); if (mConfigManager.isEnabled()) { if (MPUtility.isEmpty(breadcrumb)) { Logger.error("breadcrumb is required for leaveBreadcrumb."); @@ -630,6 +763,7 @@ public void logError(@NonNull String message) { * @param errorAttributes a Map of data attributes to associate with this error */ public void logError(@NonNull String message, @Nullable Map errorAttributes) { + MParticle.logRoktApiUsage("LOG_ERROR"); if (mConfigManager.isEnabled()) { if (MPUtility.isEmpty(message)) { Logger.error("message is required for logErrorEvent."); @@ -646,6 +780,7 @@ public void logError(@NonNull String message, @Nullable Map erro } public void logNetworkPerformance(@NonNull String url, long startTime, @NonNull String method, long length, long bytesSent, long bytesReceived, @Nullable String requestString, int responseCode) { + MParticle.logRoktApiUsage("LOG_NETWORK_PERFORMANCE"); if (mConfigManager.isEnabled()) { mAppStateManager.ensureActiveSession(); mMessageManager.logNetworkPerformanceEvent(startTime, method, url, length, bytesSent, bytesReceived, requestString); @@ -694,6 +829,7 @@ public AttributionListener getAttributionListener() { */ @NonNull public Map getAttributionResults() { + MParticle.logRoktApiUsage("GET_ATTRIBUTION_INFO"); return mKitManager.getAttributionResults(); } @@ -706,6 +842,7 @@ public Map getAttributionResults() { * @param message the name of the error event to be tracked */ public void logException(@NonNull Exception exception, @Nullable Map eventData, @Nullable String message) { + MParticle.logRoktApiUsage("LOG_EXCEPTION"); if (mConfigManager.isEnabled()) { mAppStateManager.ensureActiveSession(); JSONObject eventDataJSON = MPUtility.enforceAttributeConstraints(eventData); @@ -821,6 +958,7 @@ public void setLocation(@Nullable Location location) { * @param value the attribute value. This value will be converted to its String representation as dictated by its toString() method. */ public void setSessionAttribute(@NonNull String key, @Nullable Object value) { + MParticle.logRoktApiUsage("SET_SESSION_ATTRIBUTE"); if (key == null) { Logger.warning("setSessionAttribute called with null key. Ignoring..."); return; @@ -845,6 +983,7 @@ public void setSessionAttribute(@NonNull String key, @Nullable Object value) { * @param value the attribute value */ public void incrementSessionAttribute(@NonNull String key, int value) { + MParticle.logRoktApiUsage("INCREMENT_SESSION_ATTRIBUTE"); if (key == null) { Logger.warning("incrementSessionAttribute called with null key. Ignoring..."); return; @@ -875,6 +1014,7 @@ public Boolean getOptOut() { * @param optOutStatus set to true to opt out of event tracking */ public void setOptOut(@NonNull Boolean optOutStatus) { + MParticle.logRoktApiUsage("SET_OPT_OUT"); if (optOutStatus != null) { if (optOutStatus != mConfigManager.getOptedOut()) { if (!optOutStatus) { @@ -916,6 +1056,7 @@ public ConsentState getDeviceConsentState() { * @param state the device-level consent state, or {@code null} to clear the override */ public void setDeviceConsentState(@Nullable ConsentState state) { + MParticle.logRoktApiUsage("SET_DEVICE_CONSENT_STATE"); ConsentState oldState = mConfigManager.getEffectiveConsentState(mConfigManager.getMpid()); mConfigManager.setDeviceConsentState(state); ConsentState newState = mConfigManager.getEffectiveConsentState(mConfigManager.getMpid()); @@ -945,6 +1086,7 @@ public Boolean isDeviceBasedConsentEnabled() { */ @Nullable public Uri getSurveyUrl(final int kitId) { + MParticle.logRoktApiUsage("GET_SURVEY_URL"); return mKitManager.getSurveyUrl(kitId, null, null); } @@ -1009,6 +1151,7 @@ public void registerWebView(@NonNull WebView webView) { @SuppressLint("AddJavascriptInterface") @RequiresApi(17) public void registerWebView(@NonNull WebView webView, String requiredBridgeName) { + MParticle.logRoktApiUsage("REGISTER_WEBVIEW"); MParticleJSInterface.registerWebView(webView, requiredBridgeName); } @@ -1022,6 +1165,7 @@ public void registerWebView(@NonNull WebView webView, String requiredBridgeName) * @see MParticle.LogLevel */ public static void setLogLevel(@NonNull LogLevel level) { + MParticle.logRoktApiUsage("SET_LOG_LEVEL"); if (level != null) { Logger.setMinLogLevel(level, true); } @@ -1100,6 +1244,7 @@ public void onAudioStopped() { * @see MParticle.ServiceProviders */ public boolean isKitActive(int serviceProviderId) { + MParticle.logRoktApiUsage("IS_KIT_ACTIVE"); return mKitManager.isKitActive(serviceProviderId); } @@ -1112,6 +1257,7 @@ public boolean isKitActive(int serviceProviderId) { */ @Nullable public Object getKitInstance(int kitId) { + MParticle.logRoktApiUsage("GET_KIT_INSTANCE"); return mKitManager.getKitInstance(kitId); } @@ -1131,6 +1277,7 @@ public void logPushRegistration(@Nullable String instanceId, @Nullable String se * @param intent */ public void logNotification(@NonNull Intent intent) { + MParticle.logRoktApiUsage("LOG_NOTIFICATION"); if (mConfigManager.isEnabled()) { ProviderCloudMessage message = ProviderCloudMessage.createMessage(intent, ConfigManager.getPushKeys(mAppContext)); mMessageManager.logNotification(message, getAppState()); @@ -1161,6 +1308,7 @@ void logNotification(@NonNull ProviderCloudMessage cloudMessage, boolean startSe * @param intent */ public void logNotificationOpened(@NonNull Intent intent) { + MParticle.logRoktApiUsage("LOG_NOTIFICATION_OPENED"); logNotification(ProviderCloudMessage.createMessage(intent, ConfigManager.getPushKeys(mAppContext)), true, MParticle.getAppState(), ProviderCloudMessage.FLAG_READ | ProviderCloudMessage.FLAG_DIRECT_OPEN); } @@ -1219,6 +1367,7 @@ public IdentityApi Identity() { * @param context */ public static void reset(@NonNull Context context) { + MParticle.logRoktApiUsage("RESET"); reset(context, true, false); } @@ -1604,9 +1753,10 @@ public void onUserIdentified(MParticleUser user, MParticleUser previousUser) { } private void sendPushTokenModifyRequest(MParticleUser user, @Nullable String instanceId, @Nullable String oldInstanceId) { - Identity().modify(new Builder(user) + // Internal SDK bookkeeping (push-token refresh) — must not be reported as a partner MODIFY call. + withoutRoktApiUsage(() -> Identity().modify(new Builder(user) .pushToken(instanceId, oldInstanceId) - .build()); + .build())); } class Builder extends IdentityApiRequest.Builder { @@ -1641,6 +1791,7 @@ protected IdentityApiRequest.Builder pushToken(@Nullable String newPushToken, @N * also pass a null or empty map here to remove all of the attributes. */ public void setIntegrationAttributes(int integrationId, @Nullable Map attributes) { + MParticle.logRoktApiUsage("SET_INTEGRATION_ATTRIBUTES"); this.Internal().getConfigManager().setIntegrationAttributes(integrationId, attributes); } @@ -1658,6 +1809,7 @@ public void setIntegrationAttributes(int integrationId, @Nullable Map getIntegrationAttributes(int integrationId) { + MParticle.logRoktApiUsage("GET_INTEGRATION_ATTRIBUTES"); return this.Internal().getConfigManager().getIntegrationAttributes(integrationId); } diff --git a/android-core/src/main/java/com/mparticle/identity/IdentityApi.java b/android-core/src/main/java/com/mparticle/identity/IdentityApi.java index 0f0253e9a..0f0c865bc 100644 --- a/android-core/src/main/java/com/mparticle/identity/IdentityApi.java +++ b/android-core/src/main/java/com/mparticle/identity/IdentityApi.java @@ -108,6 +108,7 @@ public MParticleUser getUser(@NonNull Long mpid) { */ @NonNull public List getUsers() { + MParticle.logRoktApiUsage("GET_USERS"); List users = new ArrayList(); Set mpids = mConfigManager.getMpids(); mpids.remove(Constants.TEMPORARY_MPID); @@ -165,6 +166,7 @@ public MParticleTask logout() { */ @NonNull public MParticleTask logout(@Nullable final IdentityApiRequest logoutRequest) { + MParticle.logRoktApiUsage("LOGOUT"); return makeIdentityRequest(logoutRequest, new IdentityNetworkRequestRunnable() { @Override public IdentityHttpResponse request(IdentityApiRequest request) throws Exception { @@ -200,6 +202,7 @@ public MParticleTask login() { */ @NonNull public MParticleTask login(@Nullable final IdentityApiRequest loginRequest) { + MParticle.logRoktApiUsage("LOGIN"); return makeIdentityRequest(loginRequest, new IdentityNetworkRequestRunnable() { @Override public IdentityHttpResponse request(IdentityApiRequest request) throws Exception { @@ -223,6 +226,7 @@ public void onPostExecute(IdentityApiResult result) { */ @NonNull public MParticleTask identify(@Nullable final IdentityApiRequest identifyRequest) { + MParticle.logRoktApiUsage("IDENTIFY"); return makeIdentityRequest(identifyRequest, new IdentityNetworkRequestRunnable() { @Override public IdentityHttpResponse request(IdentityApiRequest request) throws Exception { @@ -246,6 +250,7 @@ public void onPostExecute(IdentityApiResult result) { */ @NonNull public BaseIdentityTask modify(@NonNull final IdentityApiRequest updateRequest) { + MParticle.logRoktApiUsage("MODIFY"); boolean devMode = MPUtility.isDevEnv() || MPUtility.isAppDebuggable(mContext); final BaseIdentityTask task = new BaseIdentityTask(); @@ -296,6 +301,7 @@ public void run() { * @return */ public boolean aliasUsers(@NonNull AliasRequest aliasRequest) { + MParticle.logRoktApiUsage("ALIAS_USERS"); if (aliasRequest.getDestinationMpid() == 0 || aliasRequest.getSourceMpid() == 0) { Logger.error("AliasRequest does not have a valid destinationMpid and a valid sourceMpid"); return false; diff --git a/android-core/src/main/java/com/mparticle/identity/MParticleUserImpl.java b/android-core/src/main/java/com/mparticle/identity/MParticleUserImpl.java index fee3063e5..c00efe9f0 100644 --- a/android-core/src/main/java/com/mparticle/identity/MParticleUserImpl.java +++ b/android-core/src/main/java/com/mparticle/identity/MParticleUserImpl.java @@ -94,21 +94,25 @@ public boolean setUserAttribute(String key, Object value) { @Override public boolean setUserAttributeList(String key, Object value) { + MParticle.logRoktApiUsage("SET_USER_ATTRIBUTE_LIST"); return mUserDelegate.setUserAttributeList(key, value, getId()); } @Override public boolean incrementUserAttribute(String key, Number value) { + MParticle.logRoktApiUsage("INCREMENT_USER_ATTRIBUTE"); return mUserDelegate.incrementUserAttribute(key, value, getId()); } @Override public boolean removeUserAttribute(String key) { + MParticle.logRoktApiUsage("REMOVE_USER_ATTRIBUTE"); return mUserDelegate.removeUserAttribute(key, getId()); } @Override public boolean setUserTag(@NonNull String tag) { + MParticle.logRoktApiUsage("SET_USER_TAG"); return setUserAttribute(tag, null); } @@ -120,11 +124,13 @@ MParticleUser setUserDelegate(MParticleUserDelegate mParticleUserDelegate) { @Override public ConsentState getConsentState() { + MParticle.logRoktApiUsage("GET_CONSENT_STATE"); return mUserDelegate.getConsentState(getId()); } @Override public void setConsentState(ConsentState state) { + MParticle.logRoktApiUsage("SET_CONSENT_STATE"); mUserDelegate.setConsentState(state, getId()); } @@ -145,6 +151,7 @@ public long getLastSeenTime() { @Override public AudienceTask getUserAudiences() { + MParticle.logRoktApiUsage("GET_USER_AUDIENCES"); return mUserDelegate.getUserAudiences(getId()); } diff --git a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java index 08be54515..678c5c6a7 100644 --- a/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java +++ b/android-core/src/main/java/com/mparticle/internal/KitFrameworkWrapper.java @@ -24,6 +24,7 @@ import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; import com.mparticle.internal.listeners.InternalListenerManager; +import com.mparticle.rokt.RoktApiDiagnosticsForwarder; import com.mparticle.rokt.RoktOptions; import org.json.JSONArray; @@ -478,6 +479,23 @@ public Object getKitInstance(int kitId) { return null; } + /** + * Forwards a bounded public-API-usage diagnostic code to the Rokt kit, but only when the kit is + * active. Core stays decoupled from kit types via the {@link RoktApiDiagnosticsForwarder} + * interface. No-op when Rokt isn't integrated/active. + */ + public void logRoktApiDiagnostic(String code) { + if (code == null || code.isEmpty()) { + return; + } + if (isKitActive(MParticle.ServiceProviders.ROKT)) { + Object kit = getKitInstance(MParticle.ServiceProviders.ROKT); + if (kit instanceof RoktApiDiagnosticsForwarder) { + ((RoktApiDiagnosticsForwarder) kit).onMParticleApiCall(code); + } + } + } + @Override public Set getSupportedKits() { if (mKitManager != null) { diff --git a/android-core/src/main/java/com/mparticle/internal/MessageHandler.java b/android-core/src/main/java/com/mparticle/internal/MessageHandler.java index 6fb47bbf6..63728320e 100644 --- a/android-core/src/main/java/com/mparticle/internal/MessageHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/MessageHandler.java @@ -246,7 +246,7 @@ public void handleMessageImpl(Message msg) { MParticle instance = MParticle.getInstance(); if (instance != null) { - instance.upload(); + MParticle.withoutRoktApiUsage(instance::upload); } } catch (MParticleApiClientImpl.MPNoConfigException ex) { Logger.error("Unable to Alias Request, API key and or API Secret is missing"); diff --git a/android-core/src/main/java/com/mparticle/internal/MessageManager.java b/android-core/src/main/java/com/mparticle/internal/MessageManager.java index c85d7aa8d..b6178506f 100644 --- a/android-core/src/main/java/com/mparticle/internal/MessageManager.java +++ b/android-core/src/main/java/com/mparticle/internal/MessageManager.java @@ -826,7 +826,7 @@ public void onFailed() { @Override public void endUploadLoop() { mUploadHandler.removeMessages(UploadHandler.UPLOAD_MESSAGES); - MParticle.getInstance().upload(); + MParticle.withoutRoktApiUsage(MParticle.getInstance()::upload); } @Override diff --git a/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java b/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java index fdbb0617c..90e2ddb7f 100644 --- a/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java +++ b/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java @@ -5,6 +5,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.mparticle.MParticle; + import java.util.concurrent.atomic.AtomicBoolean; /** @@ -37,6 +39,7 @@ public MPMediaAPI(@Nullable Context context, @NonNull MediaCallbacks callbacks) * @param playing Is your app currently playing music for the user. */ public void setAudioPlaying(boolean playing) { + MParticle.logRoktApiUsage("SET_AUDIO_PLAYING"); mAudioPlaying.set(playing); if (playing) { mCallbacks.onAudioPlaying(); @@ -46,6 +49,7 @@ public void setAudioPlaying(boolean playing) { } public boolean getAudioPlaying() { + MParticle.logRoktApiUsage("GET_AUDIO_PLAYING"); return mAudioPlaying.get(); } } diff --git a/android-core/src/main/java/com/mparticle/messaging/MPMessagingAPI.java b/android-core/src/main/java/com/mparticle/messaging/MPMessagingAPI.java index a3e4e864a..3fc164f69 100644 --- a/android-core/src/main/java/com/mparticle/messaging/MPMessagingAPI.java +++ b/android-core/src/main/java/com/mparticle/messaging/MPMessagingAPI.java @@ -8,6 +8,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.mparticle.MPService; +import com.mparticle.MParticle; import com.mparticle.internal.ConfigManager; import com.mparticle.internal.Logger; import com.mparticle.internal.MPUtility; @@ -76,10 +77,12 @@ public void enablePushNotifications(@NonNull String senderId) { * Unregister the application for FCM notifications. */ public void disablePushNotifications() { + MParticle.logRoktApiUsage("DISABLE_PUSH_NOTIFICATIONS"); ConfigManager.getInstance(mContext).clearPushRegistration(); } public void displayPushNotificationByDefault(@Nullable Boolean enabled) { + MParticle.logRoktApiUsage("DISPLAY_PUSH_NOTIFICATION_BY_DEFAULT"); ConfigManager.getInstance(mContext).setDisplayPushNotifications(enabled); } @@ -91,6 +94,7 @@ public void displayPushNotificationByDefault(@Nullable Boolean enabled) { * @see PushAnalyticsReceiver */ public void registerPushAnalyticsReceiver(@NonNull PushAnalyticsReceiver receiver) { + MParticle.logRoktApiUsage("REGISTER_PUSH_ANALYTICS_RECEIVER"); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BROADCAST_NOTIFICATION_RECEIVED); intentFilter.addAction(BROADCAST_NOTIFICATION_TAPPED); @@ -105,6 +109,7 @@ public void registerPushAnalyticsReceiver(@NonNull PushAnalyticsReceiver receive * @see PushAnalyticsReceiver */ public void unregisterPushAnalyticsReceiver(@Nullable PushAnalyticsReceiver receiver) { + MParticle.logRoktApiUsage("UNREGISTER_PUSH_ANALYTICS_RECEIVER"); LocalBroadcastManager.getInstance(mContext).unregisterReceiver(receiver); } } diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/RoktApiDiagnosticsForwarder.kt b/android-core/src/main/kotlin/com/mparticle/rokt/RoktApiDiagnosticsForwarder.kt new file mode 100644 index 000000000..4a7b8e5d0 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/rokt/RoktApiDiagnosticsForwarder.kt @@ -0,0 +1,10 @@ +package com.mparticle.rokt + +/** + * Implemented by the Rokt kit so mParticle core can forward a bounded, non-PII public-API-usage + * diagnostic code into the Rokt SDK — without core depending on any kit types. Core resolves the + * live Rokt kit via `getKitInstance` and calls this only when the kit is active. + */ +interface RoktApiDiagnosticsForwarder { + fun onMParticleApiCall(code: String) +} diff --git a/android-core/src/test/kotlin/com/mparticle/MParticleTest.kt b/android-core/src/test/kotlin/com/mparticle/MParticleTest.kt index 707808551..d89eb9564 100644 --- a/android-core/src/test/kotlin/com/mparticle/MParticleTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/MParticleTest.kt @@ -3,6 +3,11 @@ package com.mparticle import android.os.Looper import android.os.SystemClock import android.webkit.WebView +import com.mparticle.commerce.CommerceEvent +import com.mparticle.commerce.Impression +import com.mparticle.commerce.Product +import com.mparticle.commerce.Promotion +import com.mparticle.commerce.TransactionAttributes import com.mparticle.identity.IdentityApi import com.mparticle.identity.IdentityApiRequest import com.mparticle.identity.MParticleUser @@ -504,6 +509,106 @@ class MParticleTest { verify(instance.mKitManager).setWrapperSdkVersion(WrapperSdkVersion(expectedSdk, expectedVersion)) } + @Test + fun logRoktApiUsage_forwardsToKitManager_butIsSuppressedForInternalCalls() { + val instance: MParticle = InnerMockMParticle() + MParticle.setInstance(instance) + + // A genuine partner call forwards the code to the (active-kit-gated) kit manager. + MParticle.logRoktApiUsage("SELECT_PLACEMENTS") + verify(instance.mKitManager, Mockito.times(1)).logRoktApiDiagnostic("SELECT_PLACEMENTS") + + // An SDK/kit-internal call routed through withoutRoktApiUsage must NOT be reported. + MParticle.withoutRoktApiUsage { + MParticle.logRoktApiUsage("LOG_EVENT") + } + verify(instance.mKitManager, Mockito.times(0)).logRoktApiDiagnostic("LOG_EVENT") + + // Suppression is scoped: a later partner call is reported again. + MParticle.logRoktApiUsage("CLOSE") + verify(instance.mKitManager, Mockito.times(1)).logRoktApiDiagnostic("CLOSE") + } + + @Test + fun logEvent_reportsBoundedEventTypes() { + val instance: MParticle = InnerMockMParticle() + MParticle.setInstance(instance) + val expectedCodes = mutableListOf() + + linkedMapOf( + MParticle.EventType.Unknown to "LOG_EVENT_UNKNOWN", + MParticle.EventType.Navigation to "LOG_EVENT_NAVIGATION", + MParticle.EventType.Location to "LOG_EVENT_LOCATION", + MParticle.EventType.Search to "LOG_EVENT_SEARCH", + MParticle.EventType.Transaction to "LOG_EVENT_TRANSACTION", + MParticle.EventType.UserContent to "LOG_EVENT_USER_CONTENT", + MParticle.EventType.UserPreference to "LOG_EVENT_USER_PREFERENCE", + MParticle.EventType.Social to "LOG_EVENT_SOCIAL", + MParticle.EventType.Other to "LOG_EVENT_OTHER", + MParticle.EventType.Media to "LOG_EVENT_MEDIA", + ).forEach { (eventType, code) -> + instance.logEvent(MPEvent.Builder("event", eventType).shouldUploadEvent(false).build()) + expectedCodes.add(code) + } + + val product = Product.Builder("name", "sku", 1.0).build() + linkedMapOf( + Product.ADD_TO_CART to "LOG_EVENT_PRODUCT_ADD_TO_CART", + Product.REMOVE_FROM_CART to "LOG_EVENT_PRODUCT_REMOVE_FROM_CART", + Product.ADD_TO_WISHLIST to "LOG_EVENT_PRODUCT_ADD_TO_WISHLIST", + Product.REMOVE_FROM_WISHLIST to "LOG_EVENT_PRODUCT_REMOVE_FROM_WISHLIST", + Product.CHECKOUT to "LOG_EVENT_PRODUCT_CHECKOUT", + Product.CHECKOUT_OPTION to "LOG_EVENT_PRODUCT_CHECKOUT_OPTION", + Product.CLICK to "LOG_EVENT_PRODUCT_CLICK", + Product.DETAIL to "LOG_EVENT_PRODUCT_VIEW_DETAIL", + Product.PURCHASE to "LOG_EVENT_PRODUCT_PURCHASE", + Product.REFUND to "LOG_EVENT_PRODUCT_REFUND", + ).forEach { (action, code) -> + val builder = CommerceEvent.Builder(action, product).shouldUploadEvent(false) + if (action == Product.PURCHASE || action == Product.REFUND) { + builder.transactionAttributes(TransactionAttributes().setId(action)) + } + instance.logEvent(builder.build()) + expectedCodes.add(code) + } + + linkedMapOf( + Promotion.VIEW to "LOG_EVENT_PROMOTION_VIEW", + Promotion.CLICK to "LOG_EVENT_PROMOTION_CLICK", + ).forEach { (action, code) -> + instance.logEvent(CommerceEvent.Builder(action, Promotion()).shouldUploadEvent(false).build()) + expectedCodes.add(code) + } + + instance.logEvent(CommerceEvent.Builder(Impression("list", product)).shouldUploadEvent(false).build()) + expectedCodes.add("LOG_EVENT_PRODUCT_IMPRESSION") + instance.logEvent(CommerceEvent.Builder("partner-supplied", product).shouldUploadEvent(false).build()) + expectedCodes.add("LOG_EVENT_COMMERCE_OTHER") + instance.logEvent(Mockito.mock(BaseEvent::class.java)) + expectedCodes.add("LOG_EVENT_OTHER") + + val codeCaptor = ArgumentCaptor.forClass(String::class.java) + verify(instance.mKitManager, Mockito.times(expectedCodes.size)).logRoktApiDiagnostic(codeCaptor.capture()) + Assert.assertEquals(expectedCodes, codeCaptor.allValues) + } + + @Test + fun setUpdateInterval_doesNotReportItsInternalUpload() { + val instance: MParticle = InnerMockMParticle() + MParticle.setInstance(instance) + + instance.setUpdateInterval(60) + + verify(instance.mKitManager).logRoktApiDiagnostic("SET_UPLOAD_INTERVAL") + verify(instance.mKitManager, Mockito.never()).logRoktApiDiagnostic("UPLOAD") + verify(instance.mMessageManager).doUpload() + + instance.upload() + + verify(instance.mKitManager).logRoktApiDiagnostic("UPLOAD") + verify(instance.mMessageManager, Mockito.times(2)).doUpload() + } + inner class InnerMockMParticle : MParticle() { init { mConfigManager = ConfigManager(MockContext()) diff --git a/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt b/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt index 3017d6bbf..5830f4f70 100644 --- a/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt @@ -17,7 +17,7 @@ class ApiVisibilityTest { publicMethodCount++ } } - Assert.assertEquals(65, publicMethodCount) + Assert.assertEquals(67, publicMethodCount) } @Test diff --git a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt index 1804a3eb8..74cee61d8 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt @@ -12,6 +12,7 @@ import com.mparticle.WrapperSdk import com.mparticle.WrapperSdkVersion import com.mparticle.commerce.CommerceEvent import com.mparticle.internal.PushRegistrationHelper.PushRegistration +import com.mparticle.rokt.RoktApiDiagnosticsForwarder import com.mparticle.testutils.RandomUtils import org.json.JSONArray import org.junit.Assert @@ -32,6 +33,43 @@ import kotlin.test.assertEquals @RunWith(PowerMockRunner::class) class KitFrameworkWrapperTest { + private fun newWrapper(): KitFrameworkWrapper = KitFrameworkWrapper( + Mockito.mock(Context::class.java), + Mockito.mock(ReportingManager::class.java), + Mockito.mock(ConfigManager::class.java), + Mockito.mock(AppStateManager::class.java), + true, + Mockito.mock(MParticleOptions::class.java), + ) + + @Test + fun logRoktApiDiagnostic_forwardsToActiveRoktKit() { + val wrapper = newWrapper() + val mockKitManager = Mockito.mock(KitManager::class.java) + wrapper.setKitManager(mockKitManager) + val forwarder = Mockito.mock(RoktApiDiagnosticsForwarder::class.java) + `when`(mockKitManager.isKitActive(MParticle.ServiceProviders.ROKT)).thenReturn(true) + `when`(mockKitManager.getKitInstance(MParticle.ServiceProviders.ROKT)).thenReturn(forwarder) + + wrapper.logRoktApiDiagnostic("LOG_EVENT") + + verify(forwarder, times(1)).onMParticleApiCall("LOG_EVENT") + } + + @Test + fun logRoktApiDiagnostic_noOpWhenRoktKitInactive() { + val wrapper = newWrapper() + val mockKitManager = Mockito.mock(KitManager::class.java) + wrapper.setKitManager(mockKitManager) + val forwarder = Mockito.mock(RoktApiDiagnosticsForwarder::class.java) + `when`(mockKitManager.isKitActive(MParticle.ServiceProviders.ROKT)).thenReturn(false) + `when`(mockKitManager.getKitInstance(MParticle.ServiceProviders.ROKT)).thenReturn(forwarder) + + wrapper.logRoktApiDiagnostic("LOG_EVENT") + + verify(forwarder, times(0)).onMParticleApiCall(Mockito.anyString()) + } + @Test @Throws(Exception::class) fun testLoadKitLibrary() { diff --git a/android-core/src/test/kotlin/com/mparticle/internal/MessageHandlerTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/MessageHandlerTest.kt index 7f3720f97..a591143f2 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/MessageHandlerTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/MessageHandlerTest.kt @@ -60,6 +60,7 @@ class MessageHandlerTest { @Test @Throws(JSONException::class) fun testInsertAliasRequest() { + val instance = checkNotNull(MParticle.getInstance()) val insertedAliasRequest = AndroidUtils.Mutable(null) Mockito.`when`(mConfigManager.deviceApplicationStamp).thenReturn("das") val database: MParticleDBManager = @@ -88,5 +89,6 @@ class MessageHandlerTest { aliasMessage.remove(MessageKey.REQUEST_ID) insertedAliasRequest.value?.remove(MessageKey.REQUEST_ID) TestingUtils.assertJsonEqual(aliasMessage, insertedAliasRequest.value) + Mockito.verify(instance.Internal().kitManager, Mockito.never()).logRoktApiDiagnostic("UPLOAD") } } diff --git a/android-core/src/test/kotlin/com/mparticle/internal/MessageManagerTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/MessageManagerTest.kt index 36b798576..80e05ea40 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/MessageManagerTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/MessageManagerTest.kt @@ -577,6 +577,17 @@ class MessageManagerTest { ) } + @Test + fun testEndUploadLoopDoesNotReportInternalUpload() { + val instance = checkNotNull(MParticle.getInstance()) + + manager.endUploadLoop() + + Mockito.verify(uploadHandler).removeMessages(UploadHandler.UPLOAD_MESSAGES) + Mockito.verify(instance.Internal().messageManager).doUpload() + Mockito.verify(instance.Internal().kitManager, Mockito.never()).logRoktApiDiagnostic("UPLOAD") + } + @Test @Throws(Exception::class) fun testSetLocation() { diff --git a/gradle.properties b/gradle.properties index e0d20bf41..1bcb3decf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,5 +12,5 @@ JAVA_VERSION=17 # SDK VERSION) and are the single source of truth shared by the Rokt kit (com.rokt:roktsdk) and # the rokt-sdk-plus umbrella (com.rokt:payment-extension). Bump together when adopting a new # Rokt SDK release. -roktSdkVersion=6.0.1 -roktPaymentExtensionVersion=6.0.1 +roktSdkVersion=6.0.3 +roktPaymentExtensionVersion=6.0.3 diff --git a/kits/rokt/rokt/build.gradle b/kits/rokt/rokt/build.gradle index d3fa3a77a..1595b5542 100644 --- a/kits/rokt/rokt/build.gradle +++ b/kits/rokt/rokt/build.gradle @@ -82,7 +82,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0' implementation 'androidx.compose.runtime:runtime' - api "com.rokt:roktsdk:${project.findProperty('roktSdkVersion') ?: '6.0.1'}" + api "com.rokt:roktsdk:${project.findProperty('roktSdkVersion') ?: '6.0.3'}" api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testImplementation files('libs/java-json.jar') diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt index 72354c919..81b7e73fc 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/Rokt.kt @@ -33,6 +33,7 @@ class Rokt internal constructor(private val mKitManager: KitManager) { fontTypefaces: Map>? = null, config: RoktConfig? = null, ) { + MParticle.logRoktApiUsage("SELECT_PLACEMENTS") if (isEnabled()) { val resolved = resolveRoktKit() if (resolved != null) { @@ -59,10 +60,13 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * @param identifier The placement identifier to listen for events * @return A Flow emitting RoktEvent objects */ - fun events(identifier: String): Flow = if (isEnabled()) { - resolveRoktKit()?.second?.events(identifier) ?: flowOf() - } else { - flowOf() + fun events(identifier: String): Flow { + MParticle.logRoktApiUsage("ROKT_EVENTS") + return if (isEnabled()) { + resolveRoktKit()?.second?.events(identifier) ?: flowOf() + } else { + flowOf() + } } /** @@ -73,16 +77,19 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * @param paymentExtension The payment extension implementation to register * @return true if the Rokt SDK accepts the payment extension configuration */ - fun registerPaymentExtension(paymentExtension: PaymentExtension): Boolean = if (isEnabled()) { - val resolved = resolveRoktKit() - if (resolved != null) { - resolved.second.registerPaymentExtension(paymentExtension) + fun registerPaymentExtension(paymentExtension: PaymentExtension): Boolean { + MParticle.logRoktApiUsage("REGISTER_PAYMENT_EXTENSION") + return if (isEnabled()) { + val resolved = resolveRoktKit() + if (resolved != null) { + resolved.second.registerPaymentExtension(paymentExtension) + } else { + Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.") + false + } } else { - Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.") false } - } else { - false } /** @@ -98,6 +105,7 @@ class Rokt internal constructor(private val mKitManager: KitManager) { attributes: Map = emptyMap(), config: RoktConfig? = null, ) { + MParticle.logRoktApiUsage("SELECT_SHOPPABLE_ADS") if (isEnabled()) { val resolved = resolveRoktKit() if (resolved != null) { @@ -123,6 +131,7 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * @param success Whether the purchase was successful */ fun purchaseFinalized(identifier: String, catalogItemId: String, success: Boolean) { + MParticle.logRoktApiUsage("PURCHASE_FINALIZED") if (isEnabled()) { resolveRoktKit()?.second?.purchaseFinalized(identifier, catalogItemId, success) } @@ -132,6 +141,7 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * Close any active Rokt placements. */ fun close() { + MParticle.logRoktApiUsage("ROKT_CLOSE") if (isEnabled()) { resolveRoktKit()?.second?.close() } @@ -148,6 +158,7 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * @param sessionId The session id to be set. Must be a non-empty string. */ fun setSessionId(sessionId: String) { + MParticle.logRoktApiUsage("ROKT_SET_SESSION_ID") if (isEnabled()) { resolveRoktKit()?.second?.setSessionId(sessionId) } @@ -158,10 +169,13 @@ class Rokt internal constructor(private val mKitManager: KitManager) { * * @return The session id or null if no session is present or SDK is not initialized. */ - fun getSessionId(): String? = if (isEnabled()) { - resolveRoktKit()?.second?.getSessionId() - } else { - null + fun getSessionId(): String? { + MParticle.logRoktApiUsage("ROKT_GET_SESSION_ID") + return if (isEnabled()) { + resolveRoktKit()?.second?.getSessionId() + } else { + null + } } /** diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt index 08a0ee069..81aafe141 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKit.kt @@ -18,6 +18,7 @@ import com.mparticle.internal.Logger import com.mparticle.kits.KitIntegration.CommerceListener import com.mparticle.kits.KitIntegration.IdentityListener import com.mparticle.kits.KitIntegration.RoktListener +import com.mparticle.rokt.RoktApiDiagnosticsForwarder import com.rokt.roktsdk.PlacementOptions import com.rokt.roktsdk.Rokt import com.rokt.roktsdk.Rokt.SdkFrameworkType.Android @@ -53,13 +54,19 @@ class RoktKit : CommerceListener, IdentityListener, RoktListener, - RoktKitBridge { + RoktKitBridge, + RoktApiDiagnosticsForwarder { private var applicationContext: Context? = null private var hashedEmailUserIdentityType: String? = null override fun getName(): String = NAME override fun getInstance(): RoktKit = this + /** Forwards a bounded public-API-usage diagnostic code from mParticle core into the Rokt SDK. */ + override fun onMParticleApiCall(code: String) { + Rokt.logMParticleApiCall(code) + } + private var deferredAttributes: CompletableDeferred>? = null public override fun onKitCreate(settings: Map, ctx: Context): List { @@ -415,7 +422,11 @@ class RoktKit : val event = MPEvent.Builder(eventName, MParticle.EventType.Other) .customAttributes(attributes) .build() - MParticle.getInstance()?.logEvent(event) + // Kit-internal telemetry (forwarding a Rokt engagement as an mParticle event) — not a + // partner LOG_EVENT call, so suppress the usage diagnostic. + MParticle.withoutRoktApiUsage { + MParticle.getInstance()?.logEvent(event) + } } private fun getStringForIdentity(identityType: IdentityType): String = when (identityType) { diff --git a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitRequestHelper.kt b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitRequestHelper.kt index c7cdd7a64..18fccc1f3 100644 --- a/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitRequestHelper.kt +++ b/kits/rokt/rokt/src/main/kotlin/com/mparticle/kits/RoktKitRequestHelper.kt @@ -2,8 +2,10 @@ package com.mparticle.kits import android.graphics.Typeface import com.mparticle.MParticle +import com.mparticle.MParticleTask import com.mparticle.identity.IdentityApi import com.mparticle.identity.IdentityApiRequest +import com.mparticle.identity.IdentityApiResult import com.mparticle.identity.MParticleUser import com.mparticle.internal.Logger import com.mparticle.internal.MPUtility @@ -213,19 +215,26 @@ internal object RoktKitRequestHelper { } val identityRequest = identityBuilder.build() - val task = identityApi.identify(identityRequest) - - task.addFailureListener { result -> - Logger.error("Failed to sync email from selectPlacement to user: ${result?.errors}") - runnable.run() - } - - task.addSuccessListener { result -> - Logger.debug( - "Updated email identity based on selectPlacement's attributes: " + - result.user.userIdentities[MParticle.IdentityType.Email], - ) + // Kit-internal identity sync (email carried on selectPlacement) — suppress so it + // isn't reported as a partner IDENTIFY call. + var identifyTask: MParticleTask? = null + MParticle.withoutRoktApiUsage { identifyTask = identityApi.identify(identityRequest) } + val task = identifyTask + if (task == null) { runnable.run() + } else { + task.addFailureListener { result -> + Logger.error("Failed to sync email from selectPlacement to user: ${result?.errors}") + runnable.run() + } + + task.addSuccessListener { result -> + Logger.debug( + "Updated email identity based on selectPlacement's attributes: " + + result.user.userIdentities[MParticle.IdentityType.Email], + ) + runnable.run() + } } } else { runnable.run() diff --git a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt index 228e39df1..64b34db54 100644 --- a/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt +++ b/kits/rokt/rokt/src/test/kotlin/com/mparticle/kits/RoktKitTests.kt @@ -84,6 +84,17 @@ class RoktKitTests { // roktKit.configuration = KitConfiguration.createKitConfiguration(JSONObject().put("id", "-1")) } + @Test + fun onMParticleApiCall_forwards_the_code_to_Rokt_logMParticleApiCall() { + mockkObject(Rokt) + every { Rokt.logMParticleApiCall(any()) } just runs + + roktKit.onMParticleApiCall("LOG_EVENT") + + verify { Rokt.logMParticleApiCall("LOG_EVENT") } + unmockkObject(Rokt) + } + @Test fun test_prepareFinalAttributes_filters_out_null_user_attributes() { val mockFilterUser = mock(FilteredMParticleUser::class.java) diff --git a/rokt-sdk-plus/build.gradle b/rokt-sdk-plus/build.gradle index 6ed4eb63f..044e91a18 100644 --- a/rokt-sdk-plus/build.gradle +++ b/rokt-sdk-plus/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'mparticle.android.library.publish' // The umbrella tracks the mParticle SDK release line; the Rokt artifacts ride their own line. def mpVersion = (project.findProperty('VERSION') ?: '0.0.0').toString() def roktPaymentExtensionVersion = - (project.findProperty('roktPaymentExtensionVersion') ?: '6.0.1').toString() + (project.findProperty('roktPaymentExtensionVersion') ?: '6.0.3').toString() mparticleMavenPublish { groupId.set('com.rokt')