Skip to content

Commit 3163dad

Browse files
authored
Merge pull request #2339 from OneSignal/feat-custom-event
Feature: Custom Event
2 parents 1e691ee + 7e7f611 commit 3163dad

File tree

16 files changed

+613
-5
lines changed

16 files changed

+613
-5
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONUtils.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,23 @@ object JSONUtils {
163163
`object`
164164
}
165165
}
166+
167+
/**
168+
* Check if an object is JSON-serializable.
169+
* Recursively check each item if object is a map or a list.
170+
*/
171+
fun isValidJsonObject(value: Any?): Boolean {
172+
return when (value) {
173+
null,
174+
is Boolean,
175+
is Number,
176+
is String,
177+
is JSONObject,
178+
is JSONArray,
179+
-> true
180+
is Map<*, *> -> value.keys.all { it is String } && value.values.all { isValidJsonObject(it) }
181+
is List<*> -> value.all { isValidJsonObject(it) }
182+
else -> false
183+
}
184+
}
166185
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/operations/impl/OperationModelStore.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import com.onesignal.user.internal.operations.RefreshUserOperation
1414
import com.onesignal.user.internal.operations.SetAliasOperation
1515
import com.onesignal.user.internal.operations.SetPropertyOperation
1616
import com.onesignal.user.internal.operations.SetTagOperation
17+
import com.onesignal.user.internal.operations.TrackCustomEventOperation
1718
import com.onesignal.user.internal.operations.TrackPurchaseOperation
1819
import com.onesignal.user.internal.operations.TrackSessionEndOperation
1920
import com.onesignal.user.internal.operations.TrackSessionStartOperation
2021
import com.onesignal.user.internal.operations.TransferSubscriptionOperation
2122
import com.onesignal.user.internal.operations.UpdateSubscriptionOperation
23+
import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor
2224
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
2325
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor
2426
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
@@ -60,6 +62,7 @@ internal class OperationModelStore(prefs: IPreferencesService) : ModelStore<Oper
6062
UpdateUserOperationExecutor.TRACK_SESSION_START -> TrackSessionStartOperation()
6163
UpdateUserOperationExecutor.TRACK_SESSION_END -> TrackSessionEndOperation()
6264
UpdateUserOperationExecutor.TRACK_PURCHASE -> TrackPurchaseOperation()
65+
CustomEventOperationExecutor.CUSTOM_EVENT -> TrackCustomEventOperation()
6366
else -> throw Exception("Unrecognized operation: $operationName")
6467
}
6568

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/IUserManager.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,15 @@ interface IUserManager {
166166
* Remove an observer from the user state.
167167
*/
168168
fun removeObserver(observer: IUserStateObserver)
169+
170+
/**
171+
* Tracks a custom event performed by the current user
172+
*
173+
* @param name for the custom event
174+
* @param properties an optional property dictionary, must be serializable into a JSON Object
175+
*/
176+
fun trackEvent(
177+
name: String,
178+
properties: Map<String, Any>? = null,
179+
)
169180
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import com.onesignal.user.internal.backend.impl.SubscriptionBackendService
1616
import com.onesignal.user.internal.backend.impl.UserBackendService
1717
import com.onesignal.user.internal.builduser.IRebuildUserService
1818
import com.onesignal.user.internal.builduser.impl.RebuildUserService
19+
import com.onesignal.user.internal.customEvents.ICustomEventBackendService
20+
import com.onesignal.user.internal.customEvents.ICustomEventController
21+
import com.onesignal.user.internal.customEvents.impl.CustomEventBackendService
22+
import com.onesignal.user.internal.customEvents.impl.CustomEventController
1923
import com.onesignal.user.internal.identity.IdentityModelStore
2024
import com.onesignal.user.internal.migrations.RecoverConfigPushSubscription
2125
import com.onesignal.user.internal.migrations.RecoverFromDroppedLoginBug
26+
import com.onesignal.user.internal.operations.impl.executors.CustomEventOperationExecutor
2227
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
2328
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor
2429
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
@@ -71,6 +76,9 @@ internal class UserModule : IModule {
7176
builder.register<LoginUserFromSubscriptionOperationExecutor>().provides<IOperationExecutor>()
7277
builder.register<RefreshUserOperationExecutor>().provides<IOperationExecutor>()
7378
builder.register<UserManager>().provides<IUserManager>()
79+
builder.register<CustomEventController>().provides<ICustomEventController>()
80+
builder.register<CustomEventOperationExecutor>().provides<IOperationExecutor>()
81+
builder.register<CustomEventBackendService>().provides<ICustomEventBackendService>()
7482

7583
builder.register<UserRefreshService>().provides<IStartableService>()
7684

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/UserManager.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.onesignal.user.internal
22

33
import com.onesignal.common.IDManager
4+
import com.onesignal.common.JSONUtils
45
import com.onesignal.common.OneSignalUtils
56
import com.onesignal.common.events.EventProducer
67
import com.onesignal.common.modeling.ISingletonModelStoreChangeHandler
@@ -10,6 +11,7 @@ import com.onesignal.debug.LogLevel
1011
import com.onesignal.debug.internal.logging.Logging
1112
import com.onesignal.user.IUserManager
1213
import com.onesignal.user.internal.backend.IdentityConstants
14+
import com.onesignal.user.internal.customEvents.ICustomEventController
1315
import com.onesignal.user.internal.identity.IdentityModel
1416
import com.onesignal.user.internal.identity.IdentityModelStore
1517
import com.onesignal.user.internal.properties.PropertiesModel
@@ -25,6 +27,7 @@ internal open class UserManager(
2527
private val _subscriptionManager: ISubscriptionManager,
2628
private val _identityModelStore: IdentityModelStore,
2729
private val _propertiesModelStore: PropertiesModelStore,
30+
private val _customEventController: ICustomEventController,
2831
private val _languageContext: ILanguageContext,
2932
) : IUserManager, ISingletonModelStoreChangeHandler<IdentityModel> {
3033
override val onesignalId: String
@@ -244,6 +247,18 @@ internal open class UserManager(
244247
changeHandlersNotifier.unsubscribe(observer)
245248
}
246249

250+
override fun trackEvent(
251+
name: String,
252+
properties: Map<String, Any>?,
253+
) {
254+
if (!JSONUtils.isValidJsonObject(properties)) {
255+
Logging.log(LogLevel.ERROR, "Custom event properties are not JSON-serializable")
256+
return
257+
}
258+
259+
_customEventController.sendCustomEvent(name, properties)
260+
}
261+
247262
override fun onModelReplaced(
248263
model: IdentityModel,
249264
tag: String,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.onesignal.user.internal.customEvents
2+
3+
import com.onesignal.core.internal.operations.ExecutionResponse
4+
import com.onesignal.user.internal.customEvents.impl.CustomEventMetadata
5+
6+
/**
7+
* The backend service for custom events.
8+
*/
9+
interface ICustomEventBackendService {
10+
/**
11+
* Send an custom event to the backend and return the response.
12+
*
13+
* @param customEvent The custom event to send up.
14+
*/
15+
suspend fun sendCustomEvent(
16+
appId: String,
17+
onesignalId: String,
18+
externalId: String?,
19+
timestamp: Long,
20+
eventName: String,
21+
eventProperties: String?,
22+
metadata: CustomEventMetadata,
23+
): ExecutionResponse
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.onesignal.user.internal.customEvents
2+
3+
interface ICustomEventController {
4+
fun sendCustomEvent(
5+
name: String,
6+
properties: Map<String, Any>?,
7+
)
8+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.onesignal.user.internal.customEvents.impl
2+
3+
import com.onesignal.common.DateUtils
4+
import com.onesignal.common.exceptions.BackendException
5+
import com.onesignal.core.internal.http.IHttpClient
6+
import com.onesignal.core.internal.operations.ExecutionResponse
7+
import com.onesignal.core.internal.operations.ExecutionResult
8+
import com.onesignal.user.internal.customEvents.ICustomEventBackendService
9+
import org.json.JSONArray
10+
import org.json.JSONObject
11+
import java.util.TimeZone
12+
13+
internal class CustomEventBackendService(
14+
private val _httpClient: IHttpClient,
15+
) : ICustomEventBackendService {
16+
override suspend fun sendCustomEvent(
17+
appId: String,
18+
onesignalId: String,
19+
externalId: String?,
20+
timestamp: Long,
21+
eventName: String,
22+
eventProperties: String?,
23+
metadata: CustomEventMetadata,
24+
): ExecutionResponse {
25+
val body = JSONObject()
26+
body.put("name", eventName)
27+
body.put("onesignal_id", onesignalId)
28+
externalId?.let { body.put("external_id", it) }
29+
body.put(
30+
"timestamp",
31+
DateUtils.iso8601Format().apply {
32+
timeZone = TimeZone.getTimeZone("UTC")
33+
}.format(
34+
timestamp,
35+
),
36+
)
37+
38+
val payload = eventProperties?.let { JSONObject(it) } ?: JSONObject()
39+
40+
payload.put("os_sdk", metadata.toJSONObject())
41+
42+
body.put("payload", payload)
43+
val jsonObject = JSONObject().put("events", JSONArray().put(body))
44+
45+
// TODO: include auth header when identity verification is on
46+
47+
val response = _httpClient.post("apps/$appId/custom_events", jsonObject)
48+
49+
if (!response.isSuccess) {
50+
throw BackendException(response.statusCode, response.payload, response.retryAfterSeconds)
51+
}
52+
53+
return ExecutionResponse(ExecutionResult.SUCCESS)
54+
}
55+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.onesignal.user.internal.customEvents.impl
2+
3+
import com.onesignal.core.internal.config.ConfigModelStore
4+
import com.onesignal.core.internal.operations.IOperationRepo
5+
import com.onesignal.core.internal.time.ITime
6+
import com.onesignal.user.internal.customEvents.ICustomEventController
7+
import com.onesignal.user.internal.identity.IdentityModelStore
8+
import com.onesignal.user.internal.operations.TrackCustomEventOperation
9+
import org.json.JSONArray
10+
import org.json.JSONObject
11+
12+
class CustomEventController(
13+
private val _identityModelStore: IdentityModelStore,
14+
private val _configModelStore: ConfigModelStore,
15+
private val _time: ITime,
16+
private val _opRepo: IOperationRepo,
17+
) : ICustomEventController {
18+
override fun sendCustomEvent(
19+
name: String,
20+
properties: Map<String, Any>?,
21+
) {
22+
val op =
23+
TrackCustomEventOperation(
24+
_configModelStore.model.appId,
25+
_identityModelStore.model.onesignalId,
26+
_identityModelStore.model.externalId,
27+
_time.currentTimeMillis,
28+
name,
29+
properties?.let { mapToJson(it).toString() },
30+
)
31+
_opRepo.enqueue(op)
32+
}
33+
34+
/**
35+
* Recursively convert a JSON-serializable map into a JSON-compatible format, handling
36+
* nested Maps and Lists appropriately.
37+
*/
38+
private fun mapToJson(map: Map<String, Any>): JSONObject {
39+
val json = JSONObject()
40+
for ((key, value) in map) {
41+
json.put(key, convertToJson(value))
42+
}
43+
return json
44+
}
45+
46+
private fun convertToJson(value: Any): Any {
47+
return when (value) {
48+
is Map<*, *> -> {
49+
val subMap =
50+
value.entries
51+
.filter { it.key is String }
52+
.associate {
53+
it.key as String to convertToJson(it.value!!)
54+
}
55+
mapToJson(subMap)
56+
}
57+
is List<*> -> {
58+
val array = JSONArray()
59+
value.forEach { array.put(convertToJson(it!!)) }
60+
array
61+
}
62+
else -> value
63+
}
64+
}
65+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.onesignal.user.internal.customEvents.impl
2+
3+
import com.onesignal.common.putSafe
4+
import org.json.JSONException
5+
import org.json.JSONObject
6+
7+
class CustomEventMetadata(
8+
val deviceType: String?,
9+
val sdk: String?,
10+
val appVersion: String?,
11+
val type: String?,
12+
val deviceModel: String?,
13+
val deviceOS: String?,
14+
) {
15+
@Throws(JSONException::class)
16+
fun toJSONObject(): JSONObject {
17+
val json = JSONObject()
18+
json.putSafe(SDK, sdk)
19+
json.putSafe(APP_VERSION, appVersion)
20+
json.putSafe(TYPE, type)
21+
json.putSafe(DEVICE_TYPE, deviceType)
22+
json.putSafe(DEVICE_MODEL, deviceModel)
23+
json.putSafe(DEVICE_OS, deviceOS)
24+
return json
25+
}
26+
27+
override fun toString(): String {
28+
return toJSONObject().toString()
29+
}
30+
31+
companion object {
32+
private const val DEVICE_TYPE = "device_type"
33+
private const val SDK = "sdk"
34+
private const val APP_VERSION = "app_version"
35+
private const val TYPE = "type"
36+
private const val DEVICE_MODEL = "device_model"
37+
private const val DEVICE_OS = "device_os"
38+
}
39+
}

0 commit comments

Comments
 (0)