Skip to main content
Version: 1.1.x

States - Get Started

A State captures additional context that is recorded alongside your session. Unlike a one-time Event, a State's value changes over time — the SDK builds a timeline of those changes and displays it during Session Replay. States can represent anything meaningful: the current UI configuration, feed content, user preferences, network responses, or any other runtime data.

Each custom State is defined by a class that implements the CorynState interface. Custom State identifiers are separate from built-in SDK States.

Defining a State​

Your State class must:

  1. Implement CorynState and override toJsonString() to serialise itself to a JSON string.
  2. Correctly implement equals() so the SDK can detect actual changes and avoid redundant serialisation.

A Kotlin data class derives equality from its constructor properties. Include every value that affects the JSON, and keep the returned object graph stable:

SettingsState.kt
import ai.coryn.replay.shared.state.CorynState
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable

@Serializable
internal data class SettingsState(
val theme: AppTheme,
val language: String,
val ageSegment: AgeSegment
) : CorynState {
override fun toJsonString(): String {
return Json.encodeToString(serializer(), this)
}
}
Align equals() and toJsonString()

Equality must reflect the JSON value: object key order does not matter, but array order does. Repeatedly returning unequal States that produce the same JSON can cancel recording to prevent unnecessary processing and battery drain.

Keep State size reasonable

States and Events share an approximate 200 KB budget per 15-second recording segment, excluding UI captures. Exceeding this budget can omit updates until the next segment. Keep each snapshot small; a single oversized State can still use substantial memory during serialisation.

Prefer dedicated classes for large States

Use CorynStateMap for small States only — it uses reflection for JSON serialisation. For larger States, prefer a dedicated data class that implements CorynState with Kotlinx Serialization, Moshi code-gen, or a similar approach.

Stable snapshots​

Once getState() returns a State, do not mutate that instance or anything reachable from it. The SDK retains it for comparison and serialises it on a worker thread. Changing var properties, mutable collection entries, or nested objects can hide updates, race with serialisation, or repeatedly violate equality/JSON consistency. Defensive error limits can then cancel recording to protect the user's battery.

Return a new snapshot when data changes. val alone is not enough if it refers to a mutable object:

@Serializable
data class SelectionState(val selectedIds: List<String>) : CorynState {
override fun toJsonString(): String = Json.encodeToString(serializer(), this)
}

// Copy the collection so subsequent app changes cannot mutate this snapshot.
override fun getState(): SelectionState = SelectionState(selectedIds.toList())

If collection elements themselves are mutable, copy them into immutable snapshot values too. A shallow toList() or toMap() does not copy nested objects. The same contract applies to States attached to Events.

JSON validation​

toJsonString() must return valid JSON, be deterministic and be safe to call on a background thread. Invalid snapshots are skipped and reported as integration errors. Repeated invalid manually supplied States or Event payloads can cancel recording.

If a provider's getState() or serializer throws, its sample is skipped and Coryn retries it later. Other providers continue normally. Use the development Log Viewer to check custom providers before release.

State Providers​

A State Provider supplies the current State value to the SDK on demand. getState() runs frequently on the Main Thread while recording; its call frequency is not guaranteed. Keep it non-blocking and return immediately.

Implement CorynStateProvider<S> and override getState(). Register it with Coryn.provide():

UserSettingsActivity.kt
class UserSettingsActivity : AppCompatActivity(), CorynStateProvider<UserProfileState> {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Coryn.provide("UserProfile", this)
}

override fun getState() = UserProfileState(
firstname = binding?.firstname?.text?.toString() ?: "",
surname = binding?.surname?.text?.toString() ?: "",
avatarUrl = binding?.avatar?.url ?: ""
)
...
}

You can also implement the provider in a separate class. onProviderRemove() is optional and is called on the Main Thread when Coryn removes the provider (for example to clean up listeners). It is also called immediately if the recorder dependency is missing and registration cannot complete:

UserSettingsStateProvider.kt
class UserSettingsStateProvider : CorynStateProvider<UserProfileState> {

override fun getState(): UserProfileState {
return UserProfileState(...)
}

override fun onProviderRemove() {
// Clean up resources associated with this provider.
}
}

Coryn.provide(stateId, provider, lifecycle) takes these arguments; Activity and Fragment overloads supply the appropriate lifecycle:

ArgumentTypeDescription
stateIdStringHuman-readable identifier shown in the replay player, e.g. "Feed", "UserProfile".
providerCorynStateProviderThe provider instance.
lifecycleLifecycleUsed to register and unregister the provider automatically.

When an Activity or Fragment implements CorynStateProvider, use Coryn.provide(stateId, this). The ID is still explicit. Register a Fragment provider in onViewCreated(): it belongs to the view lifecycle, and registration is skipped if no current view exists.

Only one provider can own a stateId at a time. Registering another replaces the previous provider and invokes its removal callback. Keep cleanup scoped to the removed provider's resources.

Manual lifecycle management: If no Lifecycle is available, use Coryn.provideManualScopeStateProvider(stateId, provider) and call Coryn.removeManualScopeStateProvider(stateId) when the provider is no longer needed.

Map-based States​

For simple or ad-hoc States, CorynStateMap provides a ready-to-use CorynState backed by Map<String, Any?>. No custom class required.

The constructor copies the outer map. Nested values are still shared and must obey the stable snapshot contract. copy() also makes a shallow outer-map copy. Equality compares map entries regardless of insertion order; list/array ordering in the JSON remains meaningful.

Add the corresponding optional dependency first (see Get Started):

import ai.coryn.replay.api.stateserialization.gson.model.CorynStateMap
import ai.coryn.replay.api.stateserialization.gson.model.CorynStateMap.Companion.corynStateMapOf

val state = CorynStateMap(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)

// or using the helper
val state = corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)

CorynStateMap can be returned directly from a State Provider:

import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap.Companion.corynStateMapOf
import ai.coryn.replay.api.stateserialization.moshi.provider.CorynMapStateProvider

class OfferDetailsFragment : Fragment(R.layout.offer_details), CorynMapStateProvider {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Coryn.provide("OfferDetails", this)
}

override fun getState() = corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
}
  • Events — one-time captures for user actions or business metrics
  • Screens — track screen-level navigation in the session timeline
  • Screen Text — automatic text collection and privacy
  • Automatic capture — runtime context captured without custom providers