Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
adam1929 committed Apr 16, 2024
2 parents 59888a1 + c7717b4 commit fa160c9
Show file tree
Hide file tree
Showing 44 changed files with 2,954 additions and 222 deletions.
8 changes: 8 additions & 0 deletions Documentation/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
## :arrow_double_up: [SDK version update guide](./../Guides/VERSION_UPDATE.md)

## Release Notes
## Release Notes for 3.13.0
#### April 16, 2024
* Features
* Segmentation API feature support
* Bug Fixes
* Fixed: Customer Token authorization could be forced to used after anonymization


## Release Notes for 3.12.0
#### March 28, 2024
* Features
Expand Down
158 changes: 158 additions & 0 deletions Documentation/SEGMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
## Segmentation

Real-Time Segments feature personalizes the product search, category and pathway results in real-time based on customer demographic and behavioral data. More information could found [here](https://documentation.bloomreach.com/discovery/docs/real-time-customer-segments-for-discovery).
This guide provides few integration steps required to retrieve any segmentation data changes assigned to active customer.

### Using Segmentation feature

Only required step is to register one or more of your customized `SegmentationDataCallback` instances.
Each instance has to define 3 things to work:

1. Your point of interest for segmentation as field `exposingCategory`
1. Possible values are `content`, `discovery` or `merchandise`. You will get update only for segmentation data assigned to given `exposingCategory`.
2. Boolean flag to force fetch of segmentation data as field `includeFirstLoad`
1. Setting this flag to TRUE invokes segmentation data fetch immediately
2. This callback instance is notified with data even if data has not changed from last known state
3. Other callbacks are also notified but only if data has changed
4. Setting this flag to FALSE also triggers segmentation data fetch but instance is notified only if new data differs from last known state
3. Handler for new segmentation data as method `onNewData`
1. It will get all segmentation data for `exposingCategory` assigned to current customer
2. Data are list of `Segment` objects; Each `Segment` contains `id` and `segmentation_id` values

```kotlin
Exponea.registerSegmentationDataCallback(object : SegmentationDataCallback() {
override val exposingCategory = "discovery"
override val includeFirstLoad = true
override fun onNewData(segments: List<Segment>) {
Logger.i(this, "Segments: Got new segments: $segments")
}
})
```

Data payload of each `Segment` is:
```json
{
"id": "66140257f4cb337324209871",
"segmentation_id": "66140215fb50effc8a7218b4"
}
```

### When segmentation data are loaded

There are few cases when segmentation data are refreshed and this process could occur multiple times. But registered callbacks are notified only if these data has changed or if `includeFirstLoad` is TRUE. Behaviour of callback notification process is described later in this documentation with more details.
Data reload is triggered in these cases:

1. On callback instance is registered while SDK is fully initialized
2. While SDK initialization if there is any callback registered
3. On `Exponea.identifyCustomer` if is called with Hard ID
4. On any event has been tracked successfully

When segmentation data reload is triggered then process waits 5 seconds to fully start to eliminate meaningful update requests especially for higher frequency of events tracking.

> It is required to set `Exponea.flushMode` to `IMMEDIATE` value to get expected results. Process of segment calculation needs to all tracked events to be uploaded to server to calculate results effectively.
### Behaviour of callback

SDK allows you to register multiple `SegmentationDataCallback` for multiple or for same `exposingCategory`. You may register callback into SDK anytime (before and after initialization). Instances of callbacks are hold by SDK until application is terminated or until you unregister callback.
There are some principles how callback is working:

1. Callback got data assigned only for defined `exposingCategory`
2. Callback is always notified if data differs from previous reload in scope of `exposingCategory`
3. Newly registered callback is notified also for unchanged data if `includeFirstLoad` is TRUE but only once. Next callback update is called only if data has changed.
4. Unregistered callback stops listening for data change, you should consider to keep number of callbacks within reasonable value
5. Callback is notified always in background thread

### Unregistering of callback

Unregistering of callback is up to developer. SDK will hold callback instance until application is terminated otherwise.

> To unregister callback successfully you have to call `Exponea.unregisterSegmentationDataCallback` with callback instance you already registered, otherwise callback will not be unregistered.
```kotlin
val segmentCallbackInstance = object : SegmentationDataCallback() {
override val exposingCategory = "discovery"
override val includeFirstLoad = true
override fun onNewData(segments: List<Segment>) {
Logger.i(this, "Segments: Got new segments: $segments")
}
}
Exponea.registerSegmentationDataCallback(segmentCallbackInstance)
// you have to keep segmentCallbackInstance
Exponea.unregisterSegmentationDataCallback(segmentCallbackInstance)
```

Callback is able to unregister itself with `segmentCallbackInstance.unregister()` as shortcut to `Exponea.unregisterSegmentationDataCallback(segmentCallbackInstance)`.
Unregistering of callback has immediate effect.

### Listening to multiple segmentation categories
As definition of `SegmentationDataCallback` allows only one `exposingCategory` you are free to register multiple callbacks for multiple categories. As all registered callbacks are notified in background thread, you may provide collector of changed values.

```kotlin
val dataCollector = MutableLiveData<Pair<String, List<Segment>>>()
Exponea.registerSegmentationDataCallback(object : SegmentationDataCallback() {
override val exposingCategory = "discovery"
override val includeFirstLoad = false
override fun onNewData(segments: List<Segment>) {
dataCollector.postValue(Pair(exposingCategory, segments))
}
})
Exponea.registerSegmentationDataCallback(object : SegmentationDataCallback() {
override val exposingCategory = "merchandise"
override val includeFirstLoad = false
override fun onNewData(segments: List<Segment>) {
dataCollector.postValue(Pair(exposingCategory, segments))
}
})
dataCollector.observeForever {
Logger.i(this, "New data arrived! Category is ${it.first} with values ${it.second}")
// will produce example log "New data arrived! Category is discovery with values [{"id": "66140257f4cb337324209871", "segmentation_id": "66140215fb50effc8a7218b4"}]"
}
```

### Getter for segmentation data
Exponea SDK contains API to get segmentation data directly. This feature could be invoked easily by `Exponea.getSegments` usage for `exposingCategory` value.

```kotlin
Exponea.getSegments(exposingCategory) { segments ->
Logger.i(this, "Segments: Got new segments: $segments")
}
```

> Method loads segmentation data for given `exposingCategory` and currently assigned customer by `Exponea.identifyCustomer`. Bear in mind that callback is invoked in background thread.
### Logging

The SDK logs a lot of useful information on the default `INFO` level for segmentation data update. You can set the logger level using `Exponea.loggerLevel = Logger.Level.INFO` before initializing the SDK.
If you are facing any unexpected behaviour and `INFO` logs are not sufficient then try to set log level to `VERBOSE` to got more detailed information.

> Note: All logs assigned to segmentation process are prefixed with `Segments:` to bring easier search-ability to you. Bear in mind that some supporting processes (such as HTTP communication) are logging without this prefix.
#### Log examples

Process of segmentation data update may be canceled due to current state of SDK. Segmentation data are assigned to current customer and whole process is active only if there are any callbacks registered. All these validations are described in logs.

If you are not retrieving segmentation data update, you may see these logs:

- `Segments: Skipping segments update process after tracked event due to no callback registered`
- SDK tracked event successfully but there is no registered callback for segments. Please register at least one callback.
- `Segments: Adding of callback triggers fetch for no callbacks registered`
- SDK starts update for segmentation data after callback registration but this callback is missing while processing. Please ensure that you are not unregistering callback prematurely.
- `Segments: Skipping segments reload process for no callback`
- SDK is trying to reload segmentation data but there is no registered callback for segments. Please register at least one callback.
- `Segments: Skipping initial segments update process for no callback`
- SDK initialization flow tries to reload segmentation data but there is no registered callback for segments. If you want to check segmentation data on SDK init, please register at least one callback before SDK initialization.
- `Segments: Skipping initial segments update process as is not required`
- SDK initialization flow detects that all registered callbacks have `includeFirstLoad` with FALSE value. If you want to check segmentation data on SDK init, please register at least one callback with `includeFirstLoad` with TRUE value before SDK initialization.

If you are not retrieving segmentation data while registering customer, please check your usage of `Exponea.identifyCustomer` or `Exponea.anonymize`. You may face these logs:

- `Segments: Segments change check has been cancelled meanwhile`
- Segmentation data update process started but has been cancelled meanwhile by invoking of `Exponea.anonymize`. If this is unwanted behaviour, check your `Exponea.anonymize` usage.
- `Segments: Check process was canceled because customer has changed`
- Segmentation data update process started for customer but customer IDs has changed meanwhile by invoking of `Exponea.identifyCustomer` for another customer. If this is unwanted behaviour, check your `Exponea.identifyCustomer` usage.
- `Segments: Customer IDs <customer_ids> merge failed, unable to fetch segments`
- Segmentation data update process requires to link IDs but that part of process failed. Please see error logs what happen and check your `Exponea.identifyCustomer`. This part should not happen so consider to discuss it with support team.
- `Segments: New data are ignored because were loaded for different customer`
- Segmentation data update process detects that data has been fetched for previous customer. This should not lead to any problem as there is another fetch process registered for new customer, but you may face a short delay for new data retrieval. If you see this log often, check your `Exponea.identifyCustomer` usage.
- `Segments: Fetch of segments failed: <error message>`
- Please read error message carefully. This log is print if fetch of data failed by technical reason, probably network connection is not stable.
2 changes: 1 addition & 1 deletion Guides/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
2. Add ExponeaSDK dependency and sync your project
```groovy
dependencies {
implementation 'com.exponea.sdk:sdk:3.12.0'
implementation 'com.exponea.sdk:sdk:3.13.0'
}
```
3. After synchronization is complete, you can start using the SDK.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Download via Gradle:

```groovy
dependencies {
implementation 'com.exponea.sdk:sdk:3.12.0'
implementation 'com.exponea.sdk:sdk:3.13.0'
}
```

Expand All @@ -32,7 +32,7 @@ Download via Maven:
<dependency>
<groupId>com.exponea.sdk</groupId>
<artifactId>sdk</artifactId>
<version>3.12.0</version>
<version>3.13.0</version>
</dependency>
```

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ android {
applicationId "com.exponea.example"
minSdkVersion 21
targetSdkVersion 33
versionCode 79
versionName "3.12.0"
versionCode 80
versionName "3.13.0"
vectorDrawables.useSupportLibrary = true
}
compileOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import kotlinx.android.synthetic.main.fragment_fetch.consentsButton
import kotlinx.android.synthetic.main.fragment_fetch.progressBar
import kotlinx.android.synthetic.main.fragment_fetch.recommendationsButton
import kotlinx.android.synthetic.main.fragment_fetch.resultTextView
import kotlinx.android.synthetic.main.fragment_fetch.segmentationButton
import kotlinx.android.synthetic.main.fragment_fetch.view.buttonsContainer

class FetchFragment : BaseFragment() {
Expand Down Expand Up @@ -66,6 +67,14 @@ class FetchFragment : BaseFragment() {
setProgressBarVisible(true)
Exponea.getConsents({ onFetchSuccess(it) }, { onFetchFailed(it) })
}
segmentationButton.setOnClickListener {
val exposingCategory = "discovery"
Exponea.getSegments(exposingCategory) { segments ->
runOnUiThread {
resultTextView.text = "Segments for $exposingCategory category:\n$segments"
}
}
}
}

/**
Expand Down
12 changes: 10 additions & 2 deletions app/src/main/res/layout/fragment_fetch.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginTop="8dp"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
Expand All @@ -21,9 +21,10 @@
android:id="@+id/buttonsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/content_margin"
android:paddingEnd="@dimen/content_margin"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar">
Expand All @@ -43,6 +44,13 @@
android:text="Consents"
android:theme="@style/AppButton" />

<Button
android:id="@+id/segmentationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Segments"
android:theme="@style/AppButton" />

</LinearLayout>

<FrameLayout
Expand Down
9 changes: 7 additions & 2 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ android {
defaultConfig {
minSdkVersion 17
targetSdkVersion 33
buildConfigField "String", "EXPONEA_VERSION_NAME", '"3.12.0"'
buildConfigField "int", "EXPONEA_VERSION_CODE", "74"
buildConfigField "String", "EXPONEA_VERSION_NAME", '"3.13.0"'
buildConfigField "int", "EXPONEA_VERSION_CODE", "75"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'proguard-rules.pro'
}
Expand Down Expand Up @@ -99,3 +99,8 @@ dependencies {
// WorkManager testing
testImplementation 'androidx.work:work-testing:2.4.0'
}

// turn logging for unit tests
tasks.withType(Test) {
systemProperty "robolectric.logging", "stdout"
}
57 changes: 57 additions & 0 deletions sdk/src/main/java/com/exponea/sdk/Exponea.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import com.exponea.sdk.models.NotificationData
import com.exponea.sdk.models.PropertiesList
import com.exponea.sdk.models.PurchasedItem
import com.exponea.sdk.models.Result
import com.exponea.sdk.models.Segment
import com.exponea.sdk.models.SegmentationDataCallback
import com.exponea.sdk.receiver.NotificationsPermissionReceiver
import com.exponea.sdk.repository.ExponeaConfigRepository
import com.exponea.sdk.repository.PushTokenRepositoryProvider
Expand All @@ -74,6 +76,7 @@ import com.exponea.sdk.util.returnOnException
import com.exponea.sdk.view.InAppContentBlockPlaceholderView
import com.exponea.sdk.view.InAppMessagePresenter
import com.exponea.sdk.view.InAppMessageView
import java.util.concurrent.CopyOnWriteArrayList

@SuppressLint("StaticFieldLeak")
object Exponea {
Expand Down Expand Up @@ -290,6 +293,29 @@ object Exponea {
*/
var checkPushSetup: Boolean = false

/**
* Callback is notified for segmentation data updates.
*/
internal var segmentationDataCallbacks = CopyOnWriteArrayList<SegmentationDataCallback>()

/**
* Registers callback to be notified for segmentation data updates.
*/
fun registerSegmentationDataCallback(callback: SegmentationDataCallback) = runCatching {
segmentationDataCallbacks.add(callback)
getComponent()?.segmentsManager?.onCallbackAdded(callback)
return@runCatching
}.logOnException()

/**
* Unregisters callback from to be notified for segmentation data updates.
* Removing of already unregistered callback does nothing.
*/
fun unregisterSegmentationDataCallback(callback: SegmentationDataCallback) = runCatching {
segmentationDataCallbacks.removeAll { it == callback }
return@runCatching
}.logOnException()

/**
* Use this method using a file as configuration. The SDK searches for a file called
* "exponea_configuration.json" that must be inside the "assets" folder of your application
Expand Down Expand Up @@ -744,6 +770,8 @@ object Exponea {

component.inAppContentBlockManager.loadInAppContentBlockPlaceholders()

component.segmentsManager.onSdkInit()

context.addAppStateCallbacks(
onOpen = {
Logger.i(this, "App is opened")
Expand Down Expand Up @@ -1337,4 +1365,33 @@ object Exponea {
)
}
}.logOnException()

internal fun getSegmentationDataCallbacks(categoryName: String): List<SegmentationDataCallback> {
return segmentationDataCallbacks.filter { it.exposingCategory == categoryName }
}

/**
* Retrieves segmentation data for given category once.
*/
fun getSegments(
exposingCategory: String,
successCallback: ((List<Segment>) -> Unit)
) = runCatching {
initGate.waitForInitialize {
registerSegmentationDataCallback(object : SegmentationDataCallback() {
override val exposingCategory = exposingCategory
override val includeFirstLoad = true
override fun onNewData(segments: List<Segment>) {
this.unregister()
Logger.i(
this,
"Segments: Manual segmentation fetch for $exposingCategory has been done successfully"
)
runCatching {
successCallback.invoke(segments)
}.logOnException()
}
})
}
}.logOnException()
}
Loading

0 comments on commit fa160c9

Please sign in to comment.