From 2c50fa8b73def714ceb6650cf4acae84f4bd578e Mon Sep 17 00:00:00 2001 From: Josh Kasten Date: Fri, 31 Oct 2025 16:13:49 -0400 Subject: [PATCH 01/10] Merge pull request #2403 from OneSignal/kotlin1.9-update chore: Kotlin 1.9 update From bd3ac8bf770359923be884168c772e945c945474 Mon Sep 17 00:00:00 2001 From: jinliu Date: Thu, 20 Nov 2025 12:26:10 -0500 Subject: [PATCH 02/10] Merge pull request #2389 from OneSignal/chore/add_session_listener_tests chore(tests): add SessionListenerTests class From ca0ee235093f70ac177e65a3120d65e7ff2b986e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 4 Dec 2025 14:37:58 -0800 Subject: [PATCH 03/10] Merge pull request #2501 from OneSignal/fg/wrapper-prs ci: create release prs for wrappers for new android sdk release From 038ac65f75ca87a48be4c4ed255ca797269386a7 Mon Sep 17 00:00:00 2001 From: abdulraqeeb33 Date: Wed, 10 Dec 2025 13:27:22 -0500 Subject: [PATCH 04/10] test coverage test (#2409) Co-authored-by: AR Abdul Azeez --- .github/workflows/ci.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9048811fb..5fbcc01b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,11 @@ name: Build and Test SDK -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - on: pull_request: - branches: ["**"] + branches: "**" env: - DIFF_COVERAGE_THRESHOLD: "80" + DIFF_COVERAGE_THRESHOLD: '80' permissions: contents: read @@ -18,7 +14,7 @@ permissions: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: "[Checkout] Repo" uses: actions/checkout@v4 @@ -67,14 +63,14 @@ jobs: fi COVERAGE_EXIT_CODE=$? set -e # Re-enable exit on error - + # Check if markdown report was generated if [ -f "diff_coverage.md" ]; then echo "✅ Coverage report generated" else echo "⚠️ Coverage report not generated" fi - + # Only fail the build if coverage is below threshold AND not bypassed if [ "${{ steps.coverage_bypass.outputs.bypass }}" != "true" ] && [ $COVERAGE_EXIT_CODE -ne 0 ]; then echo "❌ Coverage check failed - build will fail" From bfa2e340bc0547c7041a5bef438601a67415fd3f Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Wed, 2 Jul 2025 13:14:19 -0400 Subject: [PATCH 05/10] add: custom event API and internal data model --- .../java/com/onesignal/common/JSONUtils.kt | 19 +++++++++ .../java/com/onesignal/user/IUserManager.kt | 11 +++++ .../java/com/onesignal/user/UserModule.kt | 8 ++++ .../onesignal/user/internal/UserManager.kt | 16 +++++++ .../internal/customEvents/ICustomEvent.kt | 6 +++ .../customEvents/ICustomEventController.kt | 7 ++++ .../internal/customEvents/impl/CustomEvent.kt | 42 +++++++++++++++++++ .../impl/CustomEventController.kt | 27 ++++++++++++ .../customEvents/impl/CustomEventMetadata.kt | 39 +++++++++++++++++ .../CustomEventModelStoreListener.kt | 0 10 files changed, 175 insertions(+) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt index 75ba75db7..894ec889b 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt @@ -163,4 +163,23 @@ object JSONUtils { `object` } } + + /** + * Check if an object is JSON-serializable. + * Recursively check each item if object is a map or a list. + */ + fun isValidJsonObject(value: Any?): Boolean { + return when (value) { + null, + is Boolean, + is Number, + is String, + is JSONObject, + is JSONArray, + -> true + is Map<*, *> -> value.keys.all { it is String } && value.values.all { isValidJsonObject(it) } + is List<*> -> value.all { isValidJsonObject(it) } + else -> false + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt index 2b71ca11d..7ebf37f14 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt @@ -166,4 +166,15 @@ interface IUserManager { * Remove an observer from the user state. */ fun removeObserver(observer: IUserStateObserver) + + /** + * Tracks a custom event performed by the current user + * + * @param name for the custom event + * @param properties an optional property dictionary, must be serializable into a JSON Object + */ + fun trackEvent( + name: String, + properties: Map? = null, + ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt index f8dc3c495..be5522875 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt @@ -16,9 +16,14 @@ import com.onesignal.user.internal.backend.impl.SubscriptionBackendService import com.onesignal.user.internal.backend.impl.UserBackendService import com.onesignal.user.internal.builduser.IRebuildUserService import com.onesignal.user.internal.builduser.impl.RebuildUserService +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.customEvents.ICustomEventController +import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventController import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.migrations.RecoverConfigPushSubscription import com.onesignal.user.internal.migrations.RecoverFromDroppedLoginBug +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor @@ -71,6 +76,9 @@ internal class UserModule : IModule { builder.register().provides() builder.register().provides() builder.register().provides() + builder.register().provides() + builder.register().provides() + builder.register().provides() builder.register().provides() diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index 934d23318..fbca500f6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -1,6 +1,7 @@ package com.onesignal.user.internal import com.onesignal.common.IDManager +import com.onesignal.common.JSONUtils import com.onesignal.common.OneSignalUtils import com.onesignal.common.events.EventProducer import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler @@ -10,6 +11,8 @@ import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.IUserManager import com.onesignal.user.internal.backend.IdentityConstants +import com.onesignal.user.internal.customEvents.ICustomEventController +import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.properties.PropertiesModel @@ -25,6 +28,7 @@ internal open class UserManager( private val _subscriptionManager: ISubscriptionManager, private val _identityModelStore: IdentityModelStore, private val _propertiesModelStore: PropertiesModelStore, + private val _customEventController: ICustomEventController, private val _languageContext: ILanguageContext, ) : IUserManager, ISingletonModelStoreChangeHandler { override val onesignalId: String @@ -244,6 +248,18 @@ internal open class UserManager( changeHandlersNotifier.unsubscribe(observer) } + override fun trackEvent( + name: String, + properties: Map?, + ) { + if (!JSONUtils.isValidJsonObject(properties)) { + Logging.log(LogLevel.ERROR, "Custom event properties are not JSON-serializable") + return + } + + _customEventController.sendCustomEvent(CustomEvent(name, properties)) + } + override fun onModelReplaced( model: IdentityModel, tag: String, diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt new file mode 100644 index 000000000..763fc815f --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt @@ -0,0 +1,6 @@ +package com.onesignal.user.internal.customEvents + +interface ICustomEvent { + val name: String + val properties: Map? +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt new file mode 100644 index 000000000..9c421d168 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt @@ -0,0 +1,7 @@ +package com.onesignal.user.internal.customEvents + +import com.onesignal.user.internal.customEvents.impl.CustomEvent + +interface ICustomEventController { + fun sendCustomEvent(event: CustomEvent) +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt new file mode 100644 index 000000000..5671874cb --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt @@ -0,0 +1,42 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.user.internal.customEvents.ICustomEvent +import org.json.JSONArray +import org.json.JSONObject + +class CustomEvent( + override val name: String, + override val properties: Map?, +) : ICustomEvent { + val propertiesJson: JSONObject + get() = properties?.let { mapToJson(it) } ?: JSONObject() + + private fun mapToJson(map: Map): JSONObject { + val json = JSONObject() + for ((key, value) in map) { + json.put(key, convertToJson(value)) + } + return json + } + + private fun convertToJson(value: Any?): Any? { + return when (value) { + null -> JSONObject.NULL + is Map<*, *> -> { + val subMap = + value.entries + .filter { it.key is String } + .associate { + it.key as String to convertToJson(it.value) + } + mapToJson(subMap) + } + is List<*> -> { + val array = JSONArray() + value.forEach { array.put(convertToJson(it)) } + array + } + else -> value + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt new file mode 100644 index 000000000..89e6cc280 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -0,0 +1,27 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.core.internal.config.ConfigModelStore +import com.onesignal.core.internal.operations.IOperationRepo +import com.onesignal.core.internal.time.ITime +import com.onesignal.user.internal.customEvents.ICustomEventController +import com.onesignal.user.internal.identity.IdentityModelStore +import com.onesignal.user.internal.operations.TrackEventOperation + +class CustomEventController( + private val _identityModelStore: IdentityModelStore, + private val _configModelStore: ConfigModelStore, + private val _time: ITime, + private val _opRepo: IOperationRepo, +) : ICustomEventController { + override fun sendCustomEvent(event: CustomEvent) { + val op = + TrackEventOperation( + _configModelStore.model.appId, + _identityModelStore.model.onesignalId, + _identityModelStore.model.externalId, + _time.currentTimeMillis, + event, + ) + _opRepo.enqueue(op) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt new file mode 100644 index 000000000..cd14d6a90 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventMetadata.kt @@ -0,0 +1,39 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.putSafe +import org.json.JSONException +import org.json.JSONObject + +class CustomEventMetadata( + val deviceType: String?, + val sdk: String?, + val appVersion: String?, + val type: String?, + val deviceModel: String?, + val deviceOS: String?, +) { + @Throws(JSONException::class) + fun toJSONObject(): JSONObject { + val json = JSONObject() + json.putSafe(SDK, sdk) + json.putSafe(APP_VERSION, appVersion) + json.putSafe(TYPE, type) + json.putSafe(DEVICE_TYPE, deviceType) + json.putSafe(DEVICE_MODEL, deviceModel) + json.putSafe(DEVICE_OS, deviceOS) + return json + } + + override fun toString(): String { + return toJSONObject().toString() + } + + companion object { + private const val DEVICE_TYPE = "device_type" + private const val SDK = "sdk" + private const val APP_VERSION = "app_version" + private const val TYPE = "type" + private const val DEVICE_MODEL = "device_model" + private const val DEVICE_OS = "device_os" + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt new file mode 100644 index 000000000..e69de29bb From 1bc4a1f44dab38fcfbe23d3265ff1cd7592262b4 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Wed, 9 Jul 2025 20:36:59 -0400 Subject: [PATCH 06/10] add: backend service for custom events --- .../operations/impl/OperationModelStore.kt | 3 + .../ICustomEventBackendService.kt | 24 ++++++ .../impl/CustomEventBackendService.kt | 46 +++++++++++ .../operations/TrackEventOperation.kt | 77 +++++++++++++++++++ .../executors/CustomEventOperationExecutor.kt | 72 +++++++++++++++++ .../CustomEventModelStoreListener.kt | 0 6 files changed, 222 insertions(+) create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt create mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt index 1fa25fe63..6959dd558 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt @@ -14,11 +14,13 @@ import com.onesignal.user.internal.operations.RefreshUserOperation import com.onesignal.user.internal.operations.SetAliasOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation +import com.onesignal.user.internal.operations.TrackEventOperation import com.onesignal.user.internal.operations.TrackPurchaseOperation import com.onesignal.user.internal.operations.TrackSessionEndOperation import com.onesignal.user.internal.operations.TrackSessionStartOperation import com.onesignal.user.internal.operations.TransferSubscriptionOperation import com.onesignal.user.internal.operations.UpdateSubscriptionOperation +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor @@ -60,6 +62,7 @@ internal class OperationModelStore(prefs: IPreferencesService) : ModelStore TrackSessionStartOperation() UpdateUserOperationExecutor.TRACK_SESSION_END -> TrackSessionEndOperation() UpdateUserOperationExecutor.TRACK_PURCHASE -> TrackPurchaseOperation() + CustomEventOperationExecutor.CUSTOM_EVENT -> TrackEventOperation() else -> throw Exception("Unrecognized operation: $operationName") } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt new file mode 100644 index 000000000..c59a8ad30 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt @@ -0,0 +1,24 @@ +package com.onesignal.user.internal.customEvents + +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.user.internal.customEvents.impl.CustomEvent +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata + +/** + * The backend service for custom events. + */ +interface ICustomEventBackendService { + /** + * Send an custom event to the backend and return the response. + * + * @param customEvent The custom event to send up. + */ + suspend fun sendCustomEvent( + appId: String, + onesignalId: String, + externalId: String?, + timestamp: Long, + customEvent: CustomEvent, + metadata: CustomEventMetadata, + ): ExecutionResponse +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt new file mode 100644 index 000000000..d5874460a --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt @@ -0,0 +1,46 @@ +package com.onesignal.user.internal.customEvents.impl + +import com.onesignal.common.DateUtils +import com.onesignal.common.exceptions.BackendException +import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import org.json.JSONArray +import org.json.JSONObject + +internal class CustomEventBackendService( + private val _httpClient: IHttpClient, +) : ICustomEventBackendService { + override suspend fun sendCustomEvent( + appId: String, + onesignalId: String, + externalId: String?, + timestamp: Long, + customEvent: CustomEvent, + metadata: CustomEventMetadata, + ): ExecutionResponse { + val body = JSONObject() + body.put("name", customEvent.name) + body.put("app_id", appId) + body.put("onesignal_id", onesignalId) + externalId?.let { body.put("external_id", it) } + body.put("timestamp", DateUtils.iso8601Format().format(timestamp)) + + val payload = customEvent.propertiesJson + payload.put("os_sdk", metadata.toJSONObject()) + + body.put("payload", payload) + val jsonObject = JSONObject().put("events", JSONArray().put(body)) + + // TODO: include auth header when identity verification is on + + val response = _httpClient.post("apps/$appId/custom_events", jsonObject) + + if (!response.isSuccess) { + throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) + } + + return ExecutionResponse(ExecutionResult.SUCCESS) + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt new file mode 100644 index 000000000..2496ee694 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt @@ -0,0 +1,77 @@ +package com.onesignal.user.internal.operations + +import com.onesignal.common.IDManager +import com.onesignal.core.internal.operations.GroupComparisonType +import com.onesignal.core.internal.operations.Operation +import com.onesignal.user.internal.customEvents.impl.CustomEvent +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor + +class TrackEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVENT) { + /** + * The OneSignal appId the custom event was created. + */ + var appId: String + get() = getStringProperty(::appId.name) + private set(value) { + setStringProperty(::appId.name, value) + } + + /** + * The OneSignal ID the custom event was created under. This ID *may* be locally generated + * and can be checked via [IDManager.isLocalId] to ensure correct processing. + */ + var onesignalId: String + get() = getStringProperty(::onesignalId.name) + private set(value) { + setStringProperty(::onesignalId.name, value) + } + + /** + * The optional external ID of current logged-in user. Must be unique for the [appId]. + */ + var externalId: String? + get() = getOptStringProperty(::externalId.name) + private set(value) { + setOptStringProperty(::externalId.name, value) + } + + /** + * The timestamp when the custom event was created. + */ + var timeStamp: Long + get() = getLongProperty(::timeStamp.name) + private set(value) { + setLongProperty(::timeStamp.name, value) + } + + /** + * The custom event instance containing the event name and properties. + */ + var event: CustomEvent + get() = getAnyProperty(::event.name) as CustomEvent + set(value) { + setAnyProperty(::event.name, value) + } + + override val createComparisonKey: String get() = "$appId.User.$onesignalId" + override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$name" + + // TODO: no batching of custom events until finalized + override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE + override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) + override val applyToRecordId: String get() = onesignalId + + constructor(appId: String, onesignalId: String, externalId: String?, timeStamp: Long, event: CustomEvent) : this() { + this.appId = appId + this.onesignalId = onesignalId + this.externalId = externalId + this.timeStamp = timeStamp + this.event = event + } + + override fun translateIds(map: Map) { + if (map.containsKey(onesignalId)) { + onesignalId = map[onesignalId]!! + } + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt new file mode 100644 index 000000000..dc60f1ba9 --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -0,0 +1,72 @@ +package com.onesignal.user.internal.operations.impl.executors + +import android.os.Build +import com.onesignal.common.AndroidUtils +import com.onesignal.common.NetworkUtils +import com.onesignal.common.OneSignalUtils +import com.onesignal.common.exceptions.BackendException +import com.onesignal.core.internal.application.IApplicationService +import com.onesignal.core.internal.device.IDeviceService +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.core.internal.operations.IOperationExecutor +import com.onesignal.core.internal.operations.Operation +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata +import com.onesignal.user.internal.operations.TrackEventOperation + +internal class CustomEventOperationExecutor( + private val _customEventBackendService: ICustomEventBackendService, + private val _applicationService: IApplicationService, + private val _deviceService: IDeviceService, +) : IOperationExecutor { + override val operations: List + get() = listOf(CUSTOM_EVENT) + + private val eventMetadataJson: CustomEventMetadata by lazy { + CustomEventMetadata( + deviceType = _deviceService.deviceType.name, + sdk = OneSignalUtils.SDK_VERSION, + appVersion = AndroidUtils.getAppVersion(_applicationService.appContext), + type = "AndroidPush", + deviceModel = Build.MODEL, + deviceOS = Build.VERSION.RELEASE, + ) + } + + override suspend fun execute(operations: List): ExecutionResponse { + // TODO: each trackEvent is sent individually right now; may need to batch in the future + val operation = operations.first() + + try { + when (operation) { + is TrackEventOperation -> { + _customEventBackendService.sendCustomEvent( + operation.appId, + operation.onesignalId, + operation.externalId, + operation.timeStamp, + operation.event, + eventMetadataJson, + ) + } + } + } catch (ex: BackendException) { + val responseType = NetworkUtils.getResponseStatusType(ex.statusCode) + + return when (responseType) { + NetworkUtils.ResponseStatusType.RETRYABLE -> + ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) + else -> + // TODO: will not retry all other error until we finalize how to handle + ExecutionResponse(ExecutionResult.FAIL_NORETRY) + } + } + + return ExecutionResponse(ExecutionResult.SUCCESS) + } + + companion object { + const val CUSTOM_EVENT = "custom-event" + } +} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/listeners/CustomEventModelStoreListener.kt deleted file mode 100644 index e69de29bb..000000000 From e60e1575c1b6b22a9a3ec7fdd5c30056447551db Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Sat, 19 Jul 2025 00:52:42 -0400 Subject: [PATCH 07/10] add: unit tests for custom events --- .../user/internal/UserManagerTests.kt | 51 +++++++++-- .../backend/CustomEventBackendServiceTests.kt | 86 +++++++++++++++++++ .../CustomEventOperationExecutorTests.kt | 69 +++++++++++++++ .../java/com/onesignal/mocks/MockHelper.kt | 7 ++ 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt index 41e836eb9..3f85e944e 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt @@ -4,7 +4,9 @@ import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.subscriptions.ISubscriptionManager import com.onesignal.user.internal.subscriptions.SubscriptionList +import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.every @@ -26,7 +28,7 @@ class UserManagerTests : FunSpec({ every { languageContext.language = capture(languageSlot) } answers { } val userManager = - UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), languageContext) + UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), MockHelper.customEventController(), languageContext) // When userManager.setLanguage("new-language") @@ -44,7 +46,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext()) + UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.customEventController(), MockHelper.languageContext()) // When val externalId = userManager.externalId @@ -63,7 +65,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.languageContext()) + UserManager(mockSubscriptionManager, identityModelStore, MockHelper.propertiesModelStore(), MockHelper.customEventController(), MockHelper.languageContext()) // When val alias1 = userManager.aliases["my-alias-key1"] @@ -102,7 +104,7 @@ class UserManagerTests : FunSpec({ } val userManager = - UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) + UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.customEventController(), MockHelper.languageContext()) // When val tag1 = propertiesModelStore.model.tags["my-tag-key1"] @@ -141,7 +143,7 @@ class UserManagerTests : FunSpec({ it.tags["my-tag-key1"] = "my-tag-value1" } - val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.languageContext()) + val userManager = UserManager(mockSubscriptionManager, MockHelper.identityModelStore(), propertiesModelStore, MockHelper.customEventController(), MockHelper.languageContext()) // When val tagSnapshot1 = userManager.getTags() @@ -173,6 +175,7 @@ class UserManagerTests : FunSpec({ mockSubscriptionManager, MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), + MockHelper.customEventController(), MockHelper.languageContext(), ) @@ -191,4 +194,42 @@ class UserManagerTests : FunSpec({ verify(exactly = 1) { mockSubscriptionManager.addSmsSubscription("+15558675309") } verify(exactly = 1) { mockSubscriptionManager.removeSmsSubscription("+15558675309") } } + + test("custom event controller sends various types of properties") { + // Given + val customEventController = MockHelper.customEventController() + + val userManager = + UserManager(mockk(), MockHelper.identityModelStore(), MockHelper.propertiesModelStore(), customEventController, MockHelper.languageContext()) + + val eventName = "eventName" + val properties = + mapOf( + "key1" to "value1", + "key2" to 2, + "key3" to 5.123, + "key4" to mapOf("key4-1" to "value4-1"), + "key5" to mapOf("key5-1" to mapOf("key5-1-1" to 0)), + ) + + // When + // should be able to handle any of the map structures above + shouldNotThrow { + userManager.trackEvent( + eventName, + properties, + ) + } + + // Then + // ensure the controller call sendCustomEvent() with the correct name and properties + verify(exactly = 1) { + customEventController.sendCustomEvent( + withArg { + it.name shouldBeEqual(eventName) + it.properties.toString() shouldBeEqual(properties.toString()) + }, + ) + } + } }) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt new file mode 100644 index 000000000..719deb1df --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt @@ -0,0 +1,86 @@ +package com.onesignal.user.internal.backend + +import com.onesignal.core.internal.http.HttpResponse +import com.onesignal.core.internal.http.IHttpClient +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import com.onesignal.user.internal.customEvents.impl.CustomEvent +import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.json.JSONObject + +class CustomEventBackendServiceTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + val metadata = + CustomEventMetadata( + "Android", + "sdk", + "1.0", + "type", + "deviceModel", + "deviceOS", + ) + + test("track event") { + // Given + val spyHttpClient = mockk() + coEvery { spyHttpClient.post(any(), any()) } returns HttpResponse(202, "") + val customEventBackendService = CustomEventBackendService(spyHttpClient) + + // When + val properties = + mapOf( + "proKey1" to "proVal1", + ) + val customEvent = + CustomEvent( + "event-name", + properties, + ) + + val response = + customEventBackendService.sendCustomEvent( + appId = "appId", + onesignalId = "onesignalId", + externalId = null, + timestamp = 1, + customEvent = customEvent, + metadata = metadata, + ) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify { + spyHttpClient.post( + "apps/appId/integrations/sdk/custom_events", + withArg { + val eventsObject = it.getJSONArray("events").getJSONObject(0) + val eventMap = mutableMapOf() + for (key in eventsObject.keys()) { + eventMap[key] = eventsObject.get(key) + } + + eventMap.get("name") shouldBe customEvent.name + eventMap.get("app_id") shouldBe "appId" + eventMap.get("onesignal_id") shouldBe "onesignalId" + eventMap.get("external_id") shouldBe null + eventMap.get("timestamp") shouldBe "1969-12-31T19:00:00.001Z" + + val payload = eventMap.get("payload") as JSONObject + payload.getJSONObject("os_sdk").toString() shouldBeEqual metadata.toJSONObject().toString() + payload.getString("proKey1") shouldBeEqual "proVal1" + }, + ) + } + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt new file mode 100644 index 000000000..679c19f2a --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt @@ -0,0 +1,69 @@ +package com.onesignal.user.internal.operations + +import android.content.Context +import android.os.Build +import com.onesignal.common.OneSignalUtils +import com.onesignal.core.internal.device.IDeviceService +import com.onesignal.core.internal.operations.ExecutionResponse +import com.onesignal.core.internal.operations.ExecutionResult +import com.onesignal.core.internal.operations.Operation +import com.onesignal.mocks.MockHelper +import com.onesignal.user.internal.customEvents.ICustomEventBackendService +import com.onesignal.user.internal.customEvents.impl.CustomEvent +import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk + +class CustomEventOperationExecutorTests : FunSpec({ + test("execution of track event operation") { + // Given + val mockCustomEventBackendService = mockk() + coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) + + val mockApplicationService = MockHelper.applicationService() + val mockContext = mockk(relaxed = true) + every { mockApplicationService.appContext } returns mockContext + val mockDeviceService = MockHelper.deviceService() + every { mockDeviceService.deviceType } returns IDeviceService.DeviceType.Android + + val deviceMode = Build.MODEL + val deviceOS = Build.VERSION.RELEASE + + val customEvent = + CustomEvent( + "event-name", + mapOf("key" to "value"), + ) + val customEventOperationExecutor = + CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService) + val operations = listOf(TrackEventOperation("appId", "onesignalId", null, 1, customEvent)) + + // When + val response = customEventOperationExecutor.execute(operations) + + // Then + response.result shouldBe ExecutionResult.SUCCESS + coVerify(exactly = 1) { + mockCustomEventBackendService.sendCustomEvent( + "appId", + "onesignalId", + null, + 1, + customEvent, + withArg { + it.sdk shouldBe OneSignalUtils.SDK_VERSION + it.appVersion?.shouldBeEqual("0") + it.type?.shouldBeEqual(("AndroidPush")) + it.deviceType?.shouldBeEqual(("Android")) + it.deviceModel shouldBe deviceMode + it.deviceOS shouldBe deviceOS + }, + ) + } + } +}) diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt index d8fa8ed86..a969e8b95 100644 --- a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt @@ -8,6 +8,7 @@ import com.onesignal.core.internal.language.ILanguageContext import com.onesignal.core.internal.time.ITime import com.onesignal.session.internal.session.SessionModel import com.onesignal.session.internal.session.SessionModelStore +import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.properties.PropertiesModel @@ -126,4 +127,10 @@ object MockHelper { every { deviceService.deviceType } returns IDeviceService.DeviceType.Android return deviceService } + + fun customEventController(): ICustomEventController { + val controller = mockk() + every { controller.sendCustomEvent(any()) } just runs + return controller + } } From 58a9ad30f9ff14d595cac9057f74593d66acf852 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Thu, 31 Jul 2025 00:41:22 -0400 Subject: [PATCH 08/10] fixup: removed CustomEvent and updated comparison keys - removed CustomEvent and immediately save custom event properties into a JSONObject - renamed TrackEventOperation to TrackCustomEventOperation - removed app_id in body - fix comparison key to unique custom event key - fix some test units to prevent them from failing --- .../operations/impl/OperationModelStore.kt | 4 +- .../onesignal/user/internal/UserManager.kt | 3 +- .../internal/customEvents/ICustomEvent.kt | 6 --- .../ICustomEventBackendService.kt | 4 +- .../customEvents/ICustomEventController.kt | 7 +-- .../internal/customEvents/impl/CustomEvent.kt | 42 ----------------- .../impl/CustomEventBackendService.kt | 19 ++++++-- .../impl/CustomEventController.kt | 46 +++++++++++++++++-- ...ration.kt => TrackCustomEventOperation.kt} | 29 ++++++++---- .../executors/CustomEventOperationExecutor.kt | 7 +-- .../user/internal/UserManagerTests.kt | 6 ++- .../backend/CustomEventBackendServiceTests.kt | 36 +++++++-------- .../CustomEventOperationExecutorTests.kt | 15 +++--- .../java/com/onesignal/mocks/MockHelper.kt | 2 +- 14 files changed, 117 insertions(+), 109 deletions(-) delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt delete mode 100644 OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt rename OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/{TrackEventOperation.kt => TrackCustomEventOperation.kt} (74%) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt index 6959dd558..c3d6e4591 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt @@ -14,7 +14,7 @@ import com.onesignal.user.internal.operations.RefreshUserOperation import com.onesignal.user.internal.operations.SetAliasOperation import com.onesignal.user.internal.operations.SetPropertyOperation import com.onesignal.user.internal.operations.SetTagOperation -import com.onesignal.user.internal.operations.TrackEventOperation +import com.onesignal.user.internal.operations.TrackCustomEventOperation import com.onesignal.user.internal.operations.TrackPurchaseOperation import com.onesignal.user.internal.operations.TrackSessionEndOperation import com.onesignal.user.internal.operations.TrackSessionStartOperation @@ -62,7 +62,7 @@ internal class OperationModelStore(prefs: IPreferencesService) : ModelStore TrackSessionStartOperation() UpdateUserOperationExecutor.TRACK_SESSION_END -> TrackSessionEndOperation() UpdateUserOperationExecutor.TRACK_PURCHASE -> TrackPurchaseOperation() - CustomEventOperationExecutor.CUSTOM_EVENT -> TrackEventOperation() + CustomEventOperationExecutor.CUSTOM_EVENT -> TrackCustomEventOperation() else -> throw Exception("Unrecognized operation: $operationName") } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt index fbca500f6..60f322b80 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt @@ -12,7 +12,6 @@ import com.onesignal.debug.internal.logging.Logging import com.onesignal.user.IUserManager import com.onesignal.user.internal.backend.IdentityConstants import com.onesignal.user.internal.customEvents.ICustomEventController -import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.identity.IdentityModel import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.properties.PropertiesModel @@ -257,7 +256,7 @@ internal open class UserManager( return } - _customEventController.sendCustomEvent(CustomEvent(name, properties)) + _customEventController.sendCustomEvent(name, properties) } override fun onModelReplaced( diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt deleted file mode 100644 index 763fc815f..000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEvent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.onesignal.user.internal.customEvents - -interface ICustomEvent { - val name: String - val properties: Map? -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt index c59a8ad30..92474635a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventBackendService.kt @@ -1,7 +1,6 @@ package com.onesignal.user.internal.customEvents import com.onesignal.core.internal.operations.ExecutionResponse -import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata /** @@ -18,7 +17,8 @@ interface ICustomEventBackendService { onesignalId: String, externalId: String?, timestamp: Long, - customEvent: CustomEvent, + eventName: String, + eventProperties: String?, metadata: CustomEventMetadata, ): ExecutionResponse } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt index 9c421d168..35c307539 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/ICustomEventController.kt @@ -1,7 +1,8 @@ package com.onesignal.user.internal.customEvents -import com.onesignal.user.internal.customEvents.impl.CustomEvent - interface ICustomEventController { - fun sendCustomEvent(event: CustomEvent) + fun sendCustomEvent( + name: String, + properties: Map?, + ) } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt deleted file mode 100644 index 5671874cb..000000000 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEvent.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.onesignal.user.internal.customEvents.impl - -import com.onesignal.user.internal.customEvents.ICustomEvent -import org.json.JSONArray -import org.json.JSONObject - -class CustomEvent( - override val name: String, - override val properties: Map?, -) : ICustomEvent { - val propertiesJson: JSONObject - get() = properties?.let { mapToJson(it) } ?: JSONObject() - - private fun mapToJson(map: Map): JSONObject { - val json = JSONObject() - for ((key, value) in map) { - json.put(key, convertToJson(value)) - } - return json - } - - private fun convertToJson(value: Any?): Any? { - return when (value) { - null -> JSONObject.NULL - is Map<*, *> -> { - val subMap = - value.entries - .filter { it.key is String } - .associate { - it.key as String to convertToJson(it.value) - } - mapToJson(subMap) - } - is List<*> -> { - val array = JSONArray() - value.forEach { array.put(convertToJson(it)) } - array - } - else -> value - } - } -} diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt index d5874460a..be39334e7 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt @@ -8,6 +8,7 @@ import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.user.internal.customEvents.ICustomEventBackendService import org.json.JSONArray import org.json.JSONObject +import java.util.TimeZone internal class CustomEventBackendService( private val _httpClient: IHttpClient, @@ -17,17 +18,25 @@ internal class CustomEventBackendService( onesignalId: String, externalId: String?, timestamp: Long, - customEvent: CustomEvent, + eventName: String, + eventProperties: String?, metadata: CustomEventMetadata, ): ExecutionResponse { val body = JSONObject() - body.put("name", customEvent.name) - body.put("app_id", appId) + body.put("name", eventName) body.put("onesignal_id", onesignalId) externalId?.let { body.put("external_id", it) } - body.put("timestamp", DateUtils.iso8601Format().format(timestamp)) + body.put( + "timestamp", + DateUtils.iso8601Format().apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format( + timestamp, + ), + ) + + val payload = eventProperties?.let { JSONObject(it) } ?: JSONObject() - val payload = customEvent.propertiesJson payload.put("os_sdk", metadata.toJSONObject()) body.put("payload", payload) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt index 89e6cc280..766af47fc 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -5,7 +5,9 @@ import com.onesignal.core.internal.operations.IOperationRepo import com.onesignal.core.internal.time.ITime import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModelStore -import com.onesignal.user.internal.operations.TrackEventOperation +import com.onesignal.user.internal.operations.TrackCustomEventOperation +import org.json.JSONArray +import org.json.JSONObject class CustomEventController( private val _identityModelStore: IdentityModelStore, @@ -13,15 +15,51 @@ class CustomEventController( private val _time: ITime, private val _opRepo: IOperationRepo, ) : ICustomEventController { - override fun sendCustomEvent(event: CustomEvent) { + override fun sendCustomEvent( + name: String, + properties: Map?, + ) { val op = - TrackEventOperation( + TrackCustomEventOperation( _configModelStore.model.appId, _identityModelStore.model.onesignalId, _identityModelStore.model.externalId, _time.currentTimeMillis, - event, + name, + properties?.let { mapToJson(it).toString() }, ) _opRepo.enqueue(op) } + + /** + * Recursively convert a JSON-serializable map into a JSON-compatible format, handling + * nested Maps and Lists appropriately. + */ + private fun mapToJson(map: Map): JSONObject { + val json = JSONObject() + for ((key, value) in map) { + json.put(key, convertToJson(value)) + } + return json + } + + private fun convertToJson(value: Any): Any { + return when (value) { + is Map<*, *> -> { + val subMap = + value.entries + .filter { it.key is String } + .associate { + it.key as String to convertToJson(it.value!!) + } + mapToJson(subMap) + } + is List<*> -> { + val array = JSONArray() + value.forEach { array.put(convertToJson(it!!)) } + array + } + else -> value + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt similarity index 74% rename from OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt rename to OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt index 2496ee694..eeff5c019 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackEventOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt @@ -3,10 +3,9 @@ package com.onesignal.user.internal.operations import com.onesignal.common.IDManager import com.onesignal.core.internal.operations.GroupComparisonType import com.onesignal.core.internal.operations.Operation -import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor -class TrackEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVENT) { +class TrackCustomEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVENT) { /** * The OneSignal appId the custom event was created. */ @@ -45,28 +44,38 @@ class TrackEventOperation() : Operation(CustomEventOperationExecutor.CUSTOM_EVEN } /** - * The custom event instance containing the event name and properties. + * The name for the custom event. */ - var event: CustomEvent - get() = getAnyProperty(::event.name) as CustomEvent + var eventName: String + get() = getStringProperty(::eventName.name) set(value) { - setAnyProperty(::event.name, value) + setAnyProperty(::eventName.name, value) } - override val createComparisonKey: String get() = "$appId.User.$onesignalId" - override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$name" + /** + * The nullable properties for the custom event. + */ + var eventProperties: String? + get() = getOptStringProperty(::eventProperties.name) + set(value) { + setOptStringProperty(::eventProperties.name, value) + } + + override val createComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" + override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" // TODO: no batching of custom events until finalized override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId - constructor(appId: String, onesignalId: String, externalId: String?, timeStamp: Long, event: CustomEvent) : this() { + constructor(appId: String, onesignalId: String, externalId: String?, timeStamp: Long, eventName: String, eventProperties: String?) : this() { this.appId = appId this.onesignalId = onesignalId this.externalId = externalId this.timeStamp = timeStamp - this.event = event + this.eventName = eventName + this.eventProperties = eventProperties } override fun translateIds(map: Map) { diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt index dc60f1ba9..62a5d2aa3 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -13,7 +13,7 @@ import com.onesignal.core.internal.operations.IOperationExecutor import com.onesignal.core.internal.operations.Operation import com.onesignal.user.internal.customEvents.ICustomEventBackendService import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata -import com.onesignal.user.internal.operations.TrackEventOperation +import com.onesignal.user.internal.operations.TrackCustomEventOperation internal class CustomEventOperationExecutor( private val _customEventBackendService: ICustomEventBackendService, @@ -40,13 +40,14 @@ internal class CustomEventOperationExecutor( try { when (operation) { - is TrackEventOperation -> { + is TrackCustomEventOperation -> { _customEventBackendService.sendCustomEvent( operation.appId, operation.onesignalId, operation.externalId, operation.timeStamp, - operation.event, + operation.eventName, + operation.eventProperties, eventMetadataJson, ) } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt index 3f85e944e..ada9f00f6 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/UserManagerTests.kt @@ -226,8 +226,10 @@ class UserManagerTests : FunSpec({ verify(exactly = 1) { customEventController.sendCustomEvent( withArg { - it.name shouldBeEqual(eventName) - it.properties.toString() shouldBeEqual(properties.toString()) + it.shouldBeEqual(eventName) + }, + withArg { + it.shouldBeEqual(properties) }, ) } diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt index 719deb1df..924d7f9f3 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/backend/CustomEventBackendServiceTests.kt @@ -1,11 +1,11 @@ package com.onesignal.user.internal.backend +import com.onesignal.common.DateUtils import com.onesignal.core.internal.http.HttpResponse import com.onesignal.core.internal.http.IHttpClient import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.debug.LogLevel import com.onesignal.debug.internal.logging.Logging -import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata import io.kotest.core.spec.style.FunSpec @@ -15,6 +15,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import org.json.JSONObject +import java.util.TimeZone class CustomEventBackendServiceTests : FunSpec({ beforeAny { @@ -38,15 +39,7 @@ class CustomEventBackendServiceTests : FunSpec({ val customEventBackendService = CustomEventBackendService(spyHttpClient) // When - val properties = - mapOf( - "proKey1" to "proVal1", - ) - val customEvent = - CustomEvent( - "event-name", - properties, - ) + val properties = JSONObject().put("proKey1", "proVal1").toString() val response = customEventBackendService.sendCustomEvent( @@ -54,7 +47,8 @@ class CustomEventBackendServiceTests : FunSpec({ onesignalId = "onesignalId", externalId = null, timestamp = 1, - customEvent = customEvent, + eventName = "event-name", + eventProperties = properties, metadata = metadata, ) @@ -62,7 +56,7 @@ class CustomEventBackendServiceTests : FunSpec({ response.result shouldBe ExecutionResult.SUCCESS coVerify { spyHttpClient.post( - "apps/appId/integrations/sdk/custom_events", + "apps/appId/custom_events", withArg { val eventsObject = it.getJSONArray("events").getJSONObject(0) val eventMap = mutableMapOf() @@ -70,13 +64,19 @@ class CustomEventBackendServiceTests : FunSpec({ eventMap[key] = eventsObject.get(key) } - eventMap.get("name") shouldBe customEvent.name - eventMap.get("app_id") shouldBe "appId" - eventMap.get("onesignal_id") shouldBe "onesignalId" - eventMap.get("external_id") shouldBe null - eventMap.get("timestamp") shouldBe "1969-12-31T19:00:00.001Z" + eventMap["name"] shouldBe "event-name" + eventMap["onesignal_id"] shouldBe "onesignalId" + eventMap["external_id"] shouldBe null + eventMap["timestamp"] shouldBe + DateUtils + .iso8601Format() + .apply { + timeZone = TimeZone.getTimeZone("UTC") + }.format( + 1, + ) - val payload = eventMap.get("payload") as JSONObject + val payload = eventMap["payload"] as JSONObject payload.getJSONObject("os_sdk").toString() shouldBeEqual metadata.toJSONObject().toString() payload.getString("proKey1") shouldBeEqual "proVal1" }, diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt index 679c19f2a..1b07320d0 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt @@ -9,7 +9,6 @@ import com.onesignal.core.internal.operations.ExecutionResult import com.onesignal.core.internal.operations.Operation import com.onesignal.mocks.MockHelper import com.onesignal.user.internal.customEvents.ICustomEventBackendService -import com.onesignal.user.internal.customEvents.impl.CustomEvent import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.equals.shouldBeEqual @@ -18,12 +17,13 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import org.json.JSONObject class CustomEventOperationExecutorTests : FunSpec({ test("execution of track event operation") { // Given val mockCustomEventBackendService = mockk() - coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) + coEvery { mockCustomEventBackendService.sendCustomEvent(any(), any(), any(), any(), any(), any(), any()) } returns ExecutionResponse(ExecutionResult.SUCCESS) val mockApplicationService = MockHelper.applicationService() val mockContext = mockk(relaxed = true) @@ -33,15 +33,11 @@ class CustomEventOperationExecutorTests : FunSpec({ val deviceMode = Build.MODEL val deviceOS = Build.VERSION.RELEASE + val properties = JSONObject().put("key", "value").toString() - val customEvent = - CustomEvent( - "event-name", - mapOf("key" to "value"), - ) val customEventOperationExecutor = CustomEventOperationExecutor(mockCustomEventBackendService, mockApplicationService, mockDeviceService) - val operations = listOf(TrackEventOperation("appId", "onesignalId", null, 1, customEvent)) + val operations = listOf(TrackCustomEventOperation("appId", "onesignalId", null, 1, "event-name", properties)) // When val response = customEventOperationExecutor.execute(operations) @@ -54,7 +50,8 @@ class CustomEventOperationExecutorTests : FunSpec({ "onesignalId", null, 1, - customEvent, + "event-name", + properties, withArg { it.sdk shouldBe OneSignalUtils.SDK_VERSION it.appVersion?.shouldBeEqual("0") diff --git a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt index a969e8b95..500b736e7 100644 --- a/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt +++ b/OneSignalSDK/onesignal/testhelpers/src/main/java/com/onesignal/mocks/MockHelper.kt @@ -130,7 +130,7 @@ object MockHelper { fun customEventController(): ICustomEventController { val controller = mockk() - every { controller.sendCustomEvent(any()) } just runs + every { controller.sendCustomEvent(any(), any()) } just runs return controller } } From d335b1ba2b9758667a5bf5cec241323d9278f4c9 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Wed, 21 Jan 2026 15:46:14 -0500 Subject: [PATCH 09/10] chore: update sdkVersion reference; add tests for JSONUtils --- .github/workflows/ci.yml | 14 +- .../java/com/onesignal/common/JSONUtils.kt | 36 + .../impl/CustomEventController.kt | 37 +- .../executors/CustomEventOperationExecutor.kt | 2 +- .../com/onesignal/common/JSONUtilsTests.kt | 945 ++++++++++++++++++ .../CustomEventOperationExecutorTests.kt | 2 +- 6 files changed, 994 insertions(+), 42 deletions(-) create mode 100644 OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbcc01b4..9048811fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,15 @@ name: Build and Test SDK +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: pull_request: - branches: "**" + branches: ["**"] env: - DIFF_COVERAGE_THRESHOLD: '80' + DIFF_COVERAGE_THRESHOLD: "80" permissions: contents: read @@ -14,7 +18,7 @@ permissions: jobs: build: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - name: "[Checkout] Repo" uses: actions/checkout@v4 @@ -63,14 +67,14 @@ jobs: fi COVERAGE_EXIT_CODE=$? set -e # Re-enable exit on error - + # Check if markdown report was generated if [ -f "diff_coverage.md" ]; then echo "✅ Coverage report generated" else echo "⚠️ Coverage report not generated" fi - + # Only fail the build if coverage is below threshold AND not bypassed if [ "${{ steps.coverage_bypass.outputs.bypass }}" != "true" ] && [ $COVERAGE_EXIT_CODE -ne 0 ]; then echo "❌ Coverage check failed - build will fail" diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt index 894ec889b..3169fd3d1 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt @@ -182,4 +182,40 @@ object JSONUtils { else -> false } } + + /** + * Recursively convert a JSON-serializable map into a JSON-compatible format, handling + * nested Maps and Lists appropriately. + */ + fun mapToJson(map: Map): JSONObject { + val json = JSONObject() + for ((key, value) in map) { + json.put(key, convertToJson(value)) + } + return json + } + + /** + * Recursively converts maps and lists into JSON-compatible objects, transforming maps with + * String keys into JSON objects, lists into JSON arrays, and leaving primitive values unchanged to support safe JSON serialization. + */ + fun convertToJson(value: Any): Any { + return when (value) { + is Map<*, *> -> { + val subMap = + value.entries + .filter { it.key is String } + .associate { + it.key as String to convertToJson(it.value!!) + } + mapToJson(subMap) + } + is List<*> -> { + val array = JSONArray() + value.forEach { array.put(convertToJson(it!!)) } + array + } + else -> value + } + } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt index 766af47fc..c7fec5a40 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -1,13 +1,12 @@ package com.onesignal.user.internal.customEvents.impl +import com.onesignal.common.JSONUtils import com.onesignal.core.internal.config.ConfigModelStore import com.onesignal.core.internal.operations.IOperationRepo import com.onesignal.core.internal.time.ITime import com.onesignal.user.internal.customEvents.ICustomEventController import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.TrackCustomEventOperation -import org.json.JSONArray -import org.json.JSONObject class CustomEventController( private val _identityModelStore: IdentityModelStore, @@ -26,40 +25,8 @@ class CustomEventController( _identityModelStore.model.externalId, _time.currentTimeMillis, name, - properties?.let { mapToJson(it).toString() }, + properties?.let { JSONUtils.mapToJson(it).toString() }, ) _opRepo.enqueue(op) } - - /** - * Recursively convert a JSON-serializable map into a JSON-compatible format, handling - * nested Maps and Lists appropriately. - */ - private fun mapToJson(map: Map): JSONObject { - val json = JSONObject() - for ((key, value) in map) { - json.put(key, convertToJson(value)) - } - return json - } - - private fun convertToJson(value: Any): Any { - return when (value) { - is Map<*, *> -> { - val subMap = - value.entries - .filter { it.key is String } - .associate { - it.key as String to convertToJson(it.value!!) - } - mapToJson(subMap) - } - is List<*> -> { - val array = JSONArray() - value.forEach { array.put(convertToJson(it!!)) } - array - } - else -> value - } - } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt index 62a5d2aa3..69f0c46bd 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -26,7 +26,7 @@ internal class CustomEventOperationExecutor( private val eventMetadataJson: CustomEventMetadata by lazy { CustomEventMetadata( deviceType = _deviceService.deviceType.name, - sdk = OneSignalUtils.SDK_VERSION, + sdk = OneSignalUtils.sdkVersion, appVersion = AndroidUtils.getAppVersion(_applicationService.appContext), type = "AndroidPush", deviceModel = Build.MODEL, diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt new file mode 100644 index 000000000..1320369ca --- /dev/null +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/common/JSONUtilsTests.kt @@ -0,0 +1,945 @@ +package com.onesignal.common + +import android.os.Bundle +import com.onesignal.debug.LogLevel +import com.onesignal.debug.internal.logging.Logging +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import io.mockk.every +import io.mockk.mockk +import org.json.JSONArray +import org.json.JSONObject + +class JSONUtilsTests : FunSpec({ + beforeAny { + Logging.logLevel = LogLevel.NONE + } + + context("wrapInJsonArray") { + test("should wrap a JSONObject in a JSONArray") { + // Given + val jsonObject = JSONObject().apply { + put("key", "value") + } + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.getJSONObject(0).getString("key") shouldBe "value" + } + + test("should handle null JSONObject") { + // Given + val jsonObject: JSONObject? = null + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.isNull(0) shouldBe true + } + + test("should wrap empty JSONObject") { + // Given + val jsonObject = JSONObject() + + // When + val result = JSONUtils.wrapInJsonArray(jsonObject) + + // Then + result.length() shouldBe 1 + result.getJSONObject(0).length() shouldBe 0 + } + } + + context("bundleAsJSONObject") { + test("should convert Bundle to JSONObject") { + // Given + val bundle = mockk(relaxed = true) + val keySet = setOf("stringKey", "intKey", "boolKey") + every { bundle.keySet() } returns keySet + every { bundle["stringKey"] } returns "stringValue" + every { bundle["intKey"] } returns 42 + every { bundle["boolKey"] } returns true + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.getString("stringKey") shouldBe "stringValue" + result.getInt("intKey") shouldBe 42 + result.getBoolean("boolKey") shouldBe true + } + + test("should handle empty Bundle") { + // Given + val bundle = mockk(relaxed = true) + every { bundle.keySet() } returns emptySet() + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.length() shouldBe 0 + } + + test("should handle Bundle with null values") { + // Given + val bundle = mockk(relaxed = true) + val keySet = setOf("key1", "key2") + every { bundle.keySet() } returns keySet + every { bundle["key1"] } returns "value1" + every { bundle["key2"] } returns null + + // When + val result = JSONUtils.bundleAsJSONObject(bundle) + + // Then + result.getString("key1") shouldBe "value1" + result.isNull("key2") shouldBe true + } + } + + context("jsonStringToBundle") { + test("should return null for invalid JSON string") { + // Given + val invalidJson = "{invalid json}" + + // When + val result = JSONUtils.jsonStringToBundle(invalidJson) + + // Then + result shouldBe null + } + + test("should return null for empty string") { + // Given + val emptyString = "" + + // When + val result = JSONUtils.jsonStringToBundle(emptyString) + + // Then + result shouldBe null + } + } + + context("newStringMapFromJSONObject") { + test("should convert JSONObject to Map") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", "value2") + put("key3", 123) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 3 + result["key1"] shouldBe "value1" + result["key2"] shouldBe "value2" + result["key3"] shouldBe "123" + } + + test("should handle null values as empty string") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", JSONObject.NULL) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result["key1"] shouldBe "value1" + result["key2"] shouldBe "" + } + + test("should omit nested JSONObjects") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("nested", JSONObject().apply { put("inner", "value") }) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 1 + result["key1"] shouldBe "value1" + result.containsKey("nested") shouldBe false + } + + test("should omit JSONArrays") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("array", JSONArray().put("item1").put("item2")) + } + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 1 + result["key1"] shouldBe "value1" + result.containsKey("array") shouldBe false + } + + test("should handle empty JSONObject") { + // Given + val jsonObject = JSONObject() + + // When + val result = JSONUtils.newStringMapFromJSONObject(jsonObject) + + // Then + result.size shouldBe 0 + } + } + + context("newStringSetFromJSONArray") { + test("should convert JSONArray to Set") { + // Given + val jsonArray = JSONArray().apply { + put("item1") + put("item2") + put("item3") + } + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 3 + result shouldBe setOf("item1", "item2", "item3") + } + + test("should handle empty JSONArray") { + // Given + val jsonArray = JSONArray() + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 0 + } + + test("should handle JSONArray with duplicate values") { + // Given + val jsonArray = JSONArray().apply { + put("item1") + put("item2") + put("item1") + } + + // When + val result = JSONUtils.newStringSetFromJSONArray(jsonArray) + + // Then + result.size shouldBe 2 + result shouldBe setOf("item1", "item2") + } + } + + context("toUnescapedEUIDString") { + test("should unescape forward slashes in external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user/123") + put("other_key", "value") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user/123\"" + result shouldNotContain "user\\/123" + } + + test("should handle JSON without external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("key1", "value1") + put("key2", "value2") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "key1" + result shouldContain "value1" + } + + test("should handle external_user_id without slashes") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user123") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user123\"" + } + + test("should handle multiple escaped slashes") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "user/123/456") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"user/123/456\"" + result shouldNotContain "\\/" + } + + test("should handle empty external_user_id") { + // Given + val jsonObject = JSONObject().apply { + put("external_user_id", "") + } + + // When + val result = JSONUtils.toUnescapedEUIDString(jsonObject) + + // Then + result shouldContain "\"external_user_id\":\"\"" + } + } + + context("compareJSONArrays") { + test("should return true for equal JSONArrays") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item1") + put("item2") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + + test("should return true for both null arrays") { + // When + val result = JSONUtils.compareJSONArrays(null, null) + + // Then + result shouldBe true + } + + test("should return false when one array is null") { + // Given + val array1 = JSONArray().put("item1") + + // When + val result1 = JSONUtils.compareJSONArrays(array1, null) + val result2 = JSONUtils.compareJSONArrays(null, array1) + + // Then + result1 shouldBe false + result2 shouldBe false + } + + test("should return false for arrays of different sizes") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().put("item1") + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe false + } + + test("should return false for arrays with different items") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item1") + put("item3") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe false + } + + test("should handle arrays with different order but same items") { + // Given + val array1 = JSONArray().apply { + put("item1") + put("item2") + } + val array2 = JSONArray().apply { + put("item2") + put("item1") + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + + test("should handle arrays with numbers") { + // Given + val array1 = JSONArray().apply { + put(1) + put(2) + } + val array2 = JSONArray().apply { + put(1) + put(2) + } + + // When + val result = JSONUtils.compareJSONArrays(array1, array2) + + // Then + result shouldBe true + } + } + + context("normalizeType") { + test("should convert Int to Long") { + // Given + val intValue = 42 + + // When + val result = JSONUtils.normalizeType(intValue) + + // Then + result shouldBe 42L + } + + test("should convert Float to Double") { + // Given + val floatValue = 3.14f + + // When + val result = JSONUtils.normalizeType(floatValue) + + // Then + // Float to Double conversion has precision differences, so use approximate comparison + result shouldNotBe null + val doubleValue = result as? Number + doubleValue shouldNotBe null + val difference = kotlin.math.abs(doubleValue!!.toDouble() - 3.14) + (difference < 0.0001) shouldBe true + } + + test("should return other types unchanged") { + // Given + val stringValue = "test" + val boolValue = true + val longValue = 100L + + // When + val stringResult = JSONUtils.normalizeType(stringValue) + val boolResult = JSONUtils.normalizeType(boolValue) + val longResult = JSONUtils.normalizeType(longValue) + + // Then + stringResult shouldBe "test" + boolResult shouldBe true + longResult shouldBe 100L + } + } + + context("isValidJsonObject") { + test("should return true for primitive types") { + // Then + JSONUtils.isValidJsonObject(null) shouldBe true + JSONUtils.isValidJsonObject(true) shouldBe true + JSONUtils.isValidJsonObject(false) shouldBe true + JSONUtils.isValidJsonObject(42) shouldBe true + JSONUtils.isValidJsonObject(3.14) shouldBe true + JSONUtils.isValidJsonObject("string") shouldBe true + } + + test("should return true for JSONObject and JSONArray") { + // Given + val jsonObject = JSONObject() + val jsonArray = JSONArray() + + // Then + JSONUtils.isValidJsonObject(jsonObject) shouldBe true + JSONUtils.isValidJsonObject(jsonArray) shouldBe true + } + + test("should return true for valid Map with String keys") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + "key3" to true, + ) + + // When + val result = JSONUtils.isValidJsonObject(map) + + // Then + result shouldBe true + } + + test("should return false for Map with non-String keys") { + // Given + val map = mapOf( + 1 to "value1", + 2 to "value2", + ) + + // When + val result = JSONUtils.isValidJsonObject(map) + + // Then + result shouldBe false + } + + test("should return true for valid List") { + // Given + val list = listOf("item1", "item2", 42, true) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe true + } + + test("should return true for nested valid structures") { + // Given + val nestedMap = mapOf( + "key1" to "value1", + "key2" to mapOf( + "nestedKey" to "nestedValue", + ), + "key3" to listOf("item1", "item2"), + ) + + // When + val result = JSONUtils.isValidJsonObject(nestedMap) + + // Then + result shouldBe true + } + + test("should return false for nested invalid structures") { + // Given + val invalidMap = mapOf( + "key1" to "value1", + "key2" to mapOf( + 1 to "invalid", // non-String key + ), + ) + + // When + val result = JSONUtils.isValidJsonObject(invalidMap) + + // Then + result shouldBe false + } + + test("should return false for non-JSON types") { + // Then + JSONUtils.isValidJsonObject(Any()) shouldBe false + JSONUtils.isValidJsonObject(Exception()) shouldBe false + JSONUtils.isValidJsonObject(Thread.currentThread()) shouldBe false + } + + test("should return true for List containing valid nested structures") { + // Given + val list = listOf( + "string", + 42, + mapOf("key" to "value"), + listOf("nested", "items"), + ) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe true + } + + test("should return false for List containing invalid types") { + // Given + val list = listOf( + "string", + Any(), // invalid type + ) + + // When + val result = JSONUtils.isValidJsonObject(list) + + // Then + result shouldBe false + } + } + + context("mapToJson") { + test("should convert simple map to JSONObject") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + "key3" to true, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + result.getInt("key2") shouldBe 42 + result.getBoolean("key3") shouldBe true + } + + test("should convert empty map to empty JSONObject") { + // Given + val map = emptyMap() + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.length() shouldBe 0 + } + + test("should convert map with nested map") { + // Given + val map = mapOf( + "key1" to "value1", + "nested" to mapOf( + "nestedKey1" to "nestedValue1", + "nestedKey2" to 100, + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + val nested = result.getJSONObject("nested") + nested.getString("nestedKey1") shouldBe "nestedValue1" + nested.getInt("nestedKey2") shouldBe 100 + } + + test("should convert map with list values") { + // Given + val map = mapOf( + "key1" to listOf("item1", "item2", "item3"), + "key2" to listOf(1, 2, 3), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val array1 = result.getJSONArray("key1") + array1.length() shouldBe 3 + array1.getString(0) shouldBe "item1" + array1.getString(1) shouldBe "item2" + array1.getString(2) shouldBe "item3" + + val array2 = result.getJSONArray("key2") + array2.length() shouldBe 3 + array2.getInt(0) shouldBe 1 + array2.getInt(1) shouldBe 2 + array2.getInt(2) shouldBe 3 + } + + test("should convert map with deeply nested structures") { + // Given + val map = mapOf( + "level1" to mapOf( + "level2" to mapOf( + "level3" to "deepValue", + ), + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val level1 = result.getJSONObject("level1") + val level2 = level1.getJSONObject("level2") + level2.getString("level3") shouldBe "deepValue" + } + + test("should convert map with list containing maps") { + // Given + val map = mapOf( + "items" to listOf( + mapOf("name" to "item1", "value" to 10), + mapOf("name" to "item2", "value" to 20), + ), + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + val array = result.getJSONArray("items") + array.length() shouldBe 2 + val item1 = array.getJSONObject(0) + item1.getString("name") shouldBe "item1" + item1.getInt("value") shouldBe 10 + val item2 = array.getJSONObject(1) + item2.getString("name") shouldBe "item2" + item2.getInt("value") shouldBe 20 + } + + test("should handle null values") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to JSONObject.NULL, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getString("key1") shouldBe "value1" + result.isNull("key2") shouldBe true + } + + test("should handle different number types") { + // Given + val map = mapOf( + "int" to 42, + "long" to 100L, + "double" to 3.14, + "float" to 2.5f, + ) + + // When + val result = JSONUtils.mapToJson(map) + + // Then + result.getInt("int") shouldBe 42 + result.getLong("long") shouldBe 100L + result.getDouble("double") shouldBe 3.14 + // Float precision may differ, so check approximately + (kotlin.math.abs(result.getDouble("float") - 2.5) < 0.0001) shouldBe true + } + } + + context("convertToJson") { + test("should return primitive values unchanged") { + // Then + JSONUtils.convertToJson("string") shouldBe "string" + JSONUtils.convertToJson(42) shouldBe 42 + JSONUtils.convertToJson(true) shouldBe true + JSONUtils.convertToJson(false) shouldBe false + JSONUtils.convertToJson(3.14) shouldBe 3.14 + } + + test("should convert Map to JSONObject") { + // Given + val map = mapOf( + "key1" to "value1", + "key2" to 42, + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + (result is JSONObject) shouldBe true + val jsonObject = result as JSONObject + jsonObject.getString("key1") shouldBe "value1" + jsonObject.getInt("key2") shouldBe 42 + } + + test("should convert List to JSONArray") { + // Given + val list = listOf("item1", "item2", "item3") + + // When + val result = JSONUtils.convertToJson(list) + + // Then + (result is JSONArray) shouldBe true + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 3 + jsonArray.getString(0) shouldBe "item1" + jsonArray.getString(1) shouldBe "item2" + jsonArray.getString(2) shouldBe "item3" + } + + test("should convert nested Map recursively") { + // Given + val map = mapOf( + "outer" to mapOf( + "inner" to "value", + ), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val inner = jsonObject.getJSONObject("outer") + inner.getString("inner") shouldBe "value" + } + + test("should convert List containing Maps") { + // Given + val list = listOf( + mapOf("key1" to "value1"), + mapOf("key2" to "value2"), + ) + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 2 + val item1 = jsonArray.getJSONObject(0) + item1.getString("key1") shouldBe "value1" + val item2 = jsonArray.getJSONObject(1) + item2.getString("key2") shouldBe "value2" + } + + test("should convert Map containing List") { + // Given + val map = mapOf( + "items" to listOf("a", "b", "c"), + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + val array = jsonObject.getJSONArray("items") + array.length() shouldBe 3 + array.getString(0) shouldBe "a" + array.getString(1) shouldBe "b" + array.getString(2) shouldBe "c" + } + + test("should convert empty List to empty JSONArray") { + // Given + val list = emptyList() + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 0 + } + + test("should convert empty Map to empty JSONObject") { + // Given + val map = emptyMap() + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.length() shouldBe 0 + } + + test("should handle List with mixed types") { + // Given + val list = listOf("string", 42, true, 3.14) + + // When + val result = JSONUtils.convertToJson(list) + + // Then + val jsonArray = result as JSONArray + jsonArray.length() shouldBe 4 + jsonArray.getString(0) shouldBe "string" + jsonArray.getInt(1) shouldBe 42 + jsonArray.getBoolean(2) shouldBe true + jsonArray.getDouble(3) shouldBe 3.14 + } + + test("should filter out non-String keys from Map") { + // Given + val map = mapOf( + "validKey" to "value1", + 123 to "value2", // non-String key should be filtered + ) + + // When + val result = JSONUtils.convertToJson(map) + + // Then + val jsonObject = result as JSONObject + jsonObject.length() shouldBe 1 + jsonObject.getString("validKey") shouldBe "value1" + jsonObject.has("123") shouldBe false + } + + test("should handle deeply nested structures") { + // Given + val structure = mapOf( + "level1" to listOf( + mapOf( + "level2" to listOf( + mapOf("level3" to "deepValue"), + ), + ), + ), + ) + + // When + val result = JSONUtils.convertToJson(structure) + + // Then + val jsonObject = result as JSONObject + val level1Array = jsonObject.getJSONArray("level1") + val level1Item = level1Array.getJSONObject(0) + val level2Array = level1Item.getJSONArray("level2") + val level2Item = level2Array.getJSONObject(0) + level2Item.getString("level3") shouldBe "deepValue" + } + } +}) diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt index 1b07320d0..044d4c372 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/CustomEventOperationExecutorTests.kt @@ -53,7 +53,7 @@ class CustomEventOperationExecutorTests : FunSpec({ "event-name", properties, withArg { - it.sdk shouldBe OneSignalUtils.SDK_VERSION + it.sdk shouldBe OneSignalUtils.sdkVersion it.appVersion?.shouldBeEqual("0") it.type?.shouldBeEqual(("AndroidPush")) it.deviceType?.shouldBeEqual(("Android")) From 25071caec939f3aadef72cbbedea2965f26c9323 Mon Sep 17 00:00:00 2001 From: jinliu9508 Date: Fri, 23 Jan 2026 16:02:55 -0500 Subject: [PATCH 10/10] address detekt failure --- .../impl/CustomEventBackendService.kt | 6 ++---- .../customEvents/impl/CustomEventController.kt | 18 +++++++++--------- .../operations/TrackCustomEventOperation.kt | 1 - .../executors/CustomEventOperationExecutor.kt | 14 ++++++-------- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt index be39334e7..096fa6745 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventBackendService.kt @@ -11,7 +11,7 @@ import org.json.JSONObject import java.util.TimeZone internal class CustomEventBackendService( - private val _httpClient: IHttpClient, + private val httpClient: IHttpClient, ) : ICustomEventBackendService { override suspend fun sendCustomEvent( appId: String, @@ -42,9 +42,7 @@ internal class CustomEventBackendService( body.put("payload", payload) val jsonObject = JSONObject().put("events", JSONArray().put(body)) - // TODO: include auth header when identity verification is on - - val response = _httpClient.post("apps/$appId/custom_events", jsonObject) + val response = httpClient.post("apps/$appId/custom_events", jsonObject) if (!response.isSuccess) { throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds) diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt index c7fec5a40..05142fe78 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/customEvents/impl/CustomEventController.kt @@ -9,10 +9,10 @@ import com.onesignal.user.internal.identity.IdentityModelStore import com.onesignal.user.internal.operations.TrackCustomEventOperation class CustomEventController( - private val _identityModelStore: IdentityModelStore, - private val _configModelStore: ConfigModelStore, - private val _time: ITime, - private val _opRepo: IOperationRepo, + private val identityModelStore: IdentityModelStore, + private val configModelStore: ConfigModelStore, + private val time: ITime, + private val opRepo: IOperationRepo, ) : ICustomEventController { override fun sendCustomEvent( name: String, @@ -20,13 +20,13 @@ class CustomEventController( ) { val op = TrackCustomEventOperation( - _configModelStore.model.appId, - _identityModelStore.model.onesignalId, - _identityModelStore.model.externalId, - _time.currentTimeMillis, + configModelStore.model.appId, + identityModelStore.model.onesignalId, + identityModelStore.model.externalId, + time.currentTimeMillis, name, properties?.let { JSONUtils.mapToJson(it).toString() }, ) - _opRepo.enqueue(op) + opRepo.enqueue(op) } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt index eeff5c019..73313f97e 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/TrackCustomEventOperation.kt @@ -64,7 +64,6 @@ class TrackCustomEventOperation() : Operation(CustomEventOperationExecutor.CUSTO override val createComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" override val modifyComparisonKey: String get() = "$appId.User.$onesignalId.CustomEvent.$eventName" - // TODO: no batching of custom events until finalized override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE override val canStartExecute: Boolean get() = !IDManager.isLocalId(onesignalId) override val applyToRecordId: String get() = onesignalId diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt index 69f0c46bd..2e1046e6c 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/CustomEventOperationExecutor.kt @@ -16,18 +16,18 @@ import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata import com.onesignal.user.internal.operations.TrackCustomEventOperation internal class CustomEventOperationExecutor( - private val _customEventBackendService: ICustomEventBackendService, - private val _applicationService: IApplicationService, - private val _deviceService: IDeviceService, + private val customEventBackendService: ICustomEventBackendService, + private val applicationService: IApplicationService, + private val deviceService: IDeviceService, ) : IOperationExecutor { override val operations: List get() = listOf(CUSTOM_EVENT) private val eventMetadataJson: CustomEventMetadata by lazy { CustomEventMetadata( - deviceType = _deviceService.deviceType.name, + deviceType = deviceService.deviceType.name, sdk = OneSignalUtils.sdkVersion, - appVersion = AndroidUtils.getAppVersion(_applicationService.appContext), + appVersion = AndroidUtils.getAppVersion(applicationService.appContext), type = "AndroidPush", deviceModel = Build.MODEL, deviceOS = Build.VERSION.RELEASE, @@ -35,13 +35,12 @@ internal class CustomEventOperationExecutor( } override suspend fun execute(operations: List): ExecutionResponse { - // TODO: each trackEvent is sent individually right now; may need to batch in the future val operation = operations.first() try { when (operation) { is TrackCustomEventOperation -> { - _customEventBackendService.sendCustomEvent( + customEventBackendService.sendCustomEvent( operation.appId, operation.onesignalId, operation.externalId, @@ -59,7 +58,6 @@ internal class CustomEventOperationExecutor( NetworkUtils.ResponseStatusType.RETRYABLE -> ExecutionResponse(ExecutionResult.FAIL_RETRY, retryAfterSeconds = ex.retryAfterSeconds) else -> - // TODO: will not retry all other error until we finalize how to handle ExecutionResponse(ExecutionResult.FAIL_NORETRY) } }