Add WorkflowEvent model and backend serialization#3486
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
### Motivation In a multi-step workflow paywall, toggling dark/light mode while on a non-first step would silently navigate the user back to step 1. The configuration change calls `updateState()`, which re-runs the full workflow setup — including resetting `WorkflowNavigator` to the initial step — instead of just rebuilding the cached step states for the new color scheme. ### Description Introduces `rebuildWorkflowStepStates()`, called from the configuration-change path instead of `updateState()` when the active paywall is a workflow. It: - Clears the existing `workflowStepStateCache`. - Rebuilds the current step's `PaywallState.Loaded.Components` against the new color scheme. - Leaves `WorkflowNavigator`'s `currentStepId` and `backStack` untouched, so the user stays on the step they were on. - Re-kicks the off-thread pre-warm so visited and unvisited steps repopulate with the new colors. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes workflow paywall refresh behavior to rebuild step UI state in-place instead of reloading the workflow, which could impact navigation/state caching if edge cases exist. Covered by new unit tests that assert navigation position and network call count are preserved. > > **Overview** > Fixes a workflow-paywall bug where changing the Compose `ColorScheme` (e.g., dark/light toggle) could reset multi-step workflow navigation back to the initial step. > > When a workflow is active, `refreshStateIfColorsChanged` now rebuilds workflow step states via `rebuildWorkflowStepStates`/`buildWorkflowStates` (clearing and repopulating the step cache using the *current* step) instead of calling `updateState()` and re-fetching/resetting the `WorkflowNavigator`. Adds tests ensuring the current workflow step is preserved and that `awaitGetWorkflow` is not called again on color refresh. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8472463. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Facundo Menzella <facumenzella@gmail.com> feat: track workflow step lifecycle events (step_started / step_completed) Implements the workflows_step_started and workflows_step_completed event pipeline, routing WorkflowEvent through EventsManager → BackendStoredEvent → BackendEvent.Workflows → /v1/events. - Add WorkflowEvent sealed class (StepStarted / StepCompleted) implementing FeatureEvent; @SerialName discriminators ensure stable serialisation - Add BackendEvent.Workflows wire model with nested Context and Properties - Register BackendStoredEvent.Workflows in both JsonProvider and EventsManager polymorphic modules - Add WorkflowEvent.toBackendStoredEvent() converter - Track step_started on successful initial workflow load (guarded so a failed load does not fire spurious events) - Track step_completed + step_started on forward and back navigation - Track step_completed (toStepId = null) on paywall close - Track step_completed and clear traceId when workflow state is cleared on error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> feat: align workflow event properties with monetization event naming spec Remove trace_id (not in spec) and add workflow_type ("paywall"), step_type (from WorkflowStep.type), and screen_type (from WorkflowStep.screenType, defaults to emptyList) to both StepStarted and StepCompleted events and their BackendEvent wire shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> test: add non-empty screen_type coverage for workflow events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> fix: remove workflow_type/step_type/screen_type from BackendEvent wire model Khepri derives these from its own DB at event time — no need for the SDK to send them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
be17d14 to
3f91e95
Compare
| @OptIn(InternalRevenueCatAPI::class) | ||
| @JvmSynthetic | ||
| internal fun WorkflowEvent.toBackendStoredEvent( | ||
| appUserID: String, |
There was a problem hiding this comment.
is it intentional to not have appSessionID? Every other event has it
There was a problem hiding this comment.
yes, intentional. PaywallEvent has an appSessionID but not the workflow events.
There was a problem hiding this comment.
actually, I see there's a trace_id I am not passing. Adding that and will confirm with Dan the behavior, if it should be reset on every app session, or on every workflow
There was a problem hiding this comment.
confirmed.
So a trace_id is intended to be specific to a single impression of a workflow & whatever events happen as a result of that impression, while an app_session_id could span multiple distinct workflow impressions (and therefore distinct trace_id's) and allow you to group them together into a single session that a user had in an app.
| public abstract val stepId: String | ||
| public abstract val workflowType: String? | ||
| public abstract val stepType: String? | ||
| public abstract val screenType: List<String> |
There was a problem hiding this comment.
does this come from the backend? 🤔
There was a problem hiding this comment.
I am confused by stepType + screenType
There was a problem hiding this comment.
check this conversation for reference. https://revenuecat.slack.com/archives/C0AGDQGUZ97/p1778696574907729?thread_ts=1776863598.664489&cid=C0AGDQGUZ97
We can remove workflowType, stepType, and screenType since they are populated server side
facumenzella
left a comment
There was a problem hiding this comment.
a few questions, but looks ok
These fields are derived server-side from the workflow/step IDs and do not need to be sent by the client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0c39a3e. Configure here.
Khepri uses Field(discriminator='type') on the analytics events Pydantic union, so 'type':'workflows' must be present in the JSON payload for the event to be routed correctly. With encodeDefaults=false, fields with default values were silently dropped — sending 'discriminator':'workflows' (kotlinx local discriminator) but no 'type' field, causing all workflow events to fail Pydantic validation at the backend. Remove the default values from BackendEvent.Workflows.version and .type so they are always serialized, matching the pattern used by Paywalls/Ad events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Workflow events have no app_session_id field in khepri's schema — the intended correlation mechanism is trace_id. Reuse the existing appSessionID (generated once per SDK init in EventsManager) as the trace_id so all workflow events from a session can be correlated with each other and with other event types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
trace_id is scoped to a single workflow impression (like paywall's sessionIdentifier), not the app session. Move it onto WorkflowEvent itself so the caller generates one UUID per workflow entry and threads it through all step events for that impression. The appSessionID approach was incorrect — an app session can span multiple distinct workflow impressions, each needing its own trace_id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3486 +/- ##
==========================================
+ Coverage 79.91% 79.96% +0.05%
==========================================
Files 369 370 +1
Lines 14934 15014 +80
Branches 2058 2071 +13
==========================================
+ Hits 11934 12006 +72
- Misses 2163 2167 +4
- Partials 837 841 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
#6858) * feat(workflows): add WorkflowEvent model and wire format serialization Port of RevenueCat/purchases-android#3486 Adds WorkflowEvent (stepStarted/stepCompleted) as the iOS domain model for workflow step lifecycle events. Adds FeatureEventsRequest.WorkflowEvent as the Khepri-compatible wire format, wires it into the Feature routing pipeline, and exposes screenType on WorkflowStep for local context. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: register new workflow event files in RevenueCat.xcodeproj Adds WorkflowEvent.swift, FeatureEventsRequest+WorkflowEvent.swift, WorkflowEventTests.swift, and WorkflowEventsRequestTests.swift to the committed SPM-generated Xcode project so CI builds pick them up. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(workflows): address review feedback on WorkflowEvent wire format - Rename `discriminator` to `type` in FeatureEventsRequest.WorkflowEvent (matches backend expectation and CustomPaywallEvent pattern) - Add `version: Int = 1` to wire format, matching Android and CustomPaywallEvent schema versioning - Add `experimentId`, `experimentVariant`, `isLastVariantStep` to WorkflowEvent.Data and wire format Properties (parity with Android/Khepri) - Add `traceId` to WorkflowEvent.Data and wire format Properties (parity with Android) - Update tests to cover renamed field, version, new experiment fields, and traceId in JSON shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(workflows): handle WorkflowEvent in toMap() and remove shadowing test extension - Add WorkflowEvent case to FeatureEvent.toMap() so eventsListener receives structured data instead of the unknown fallback - Remove redundant private extension in WorkflowEventTests that shadowed the @_spi(Internal) public creationData/data accessors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(workflows): address review items - locale in wire format, toMap fields, screenType accessor - Add localeIdentifier to WorkflowEvent.Data (captured at creation, always sent in wire format context) - Change Context.locale from String? to String; wire format always includes locale - Add trace_id, experiment_id, experiment_variant, is_last_variant_step to workflowEventMap() - Add public stepScreenType accessor on WorkflowStep for SPI consumers - Add tests for locale in wire format and all workflowEventMap() fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(workflows): include locale in workflowEventMap() for hybrid SDK consistency Matches paywallMap(), customerCenterImpressionMap(), and customerCenterAnswerSubmittedMap() which all include locale. Hybrid SDK consumers now receive locale for workflow events. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
**This is an automatic release.** ## RevenueCat SDK ### ✨ New Features * Add presented offering context to custom paywall events (RevenueCat#3424) via Rick (@rickvdl) * Add Workflows list endpoint (RevenueCat#3509) via Cesar de la Vega (@vegaro) ## RevenueCatUI SDK ### Paywalls_v2 #### 🐞 Bugfixes * Fix 1px seam between sliding multipage paywall pages (RevenueCat#3526) via Cesar de la Vega (@vegaro) ### 🔄 Other Changes * refactor: extract Offering.presentedOfferingContext() helper and apply across SDK (RevenueCat#3513) via Rick (@rickvdl) * Add JSON Logic string + array operators (RevenueCat#3485) via Antonio Pallares (@ajpallares) * Add ForbiddenPublicSealedClass detekt rule (RevenueCat#3503) via Toni Rico (@tonidero) * Update baseline profiles (RevenueCat#3519) via RevenueCat Git Bot (@RCGitBot) * build(deps): bump fastlane-plugin-revenuecat_internal from `af7bb5c` to `ce6a7ef` (RevenueCat#3515) via dependabot[bot] (@dependabot[bot]) * Add JSON Logic comparison operators (<, <=, >, >=) (RevenueCat#3484) via Antonio Pallares (@ajpallares) * Add JSON Logic arithmetic operators (+, -, *, /, %) (RevenueCat#3483) via Antonio Pallares (@ajpallares) * Add WorkflowEvent model and backend serialization (RevenueCat#3486) via Cesar de la Vega (@vegaro) * RulesEngine: add JSON Logic predicate evaluator (RevenueCat#3482) via Antonio Pallares (@ajpallares) * Add :rules-engine-internal skeleton module (RevenueCat#3478) via Antonio Pallares (@ajpallares) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Version bump and changelog/docs/CI path updates only; no application logic changes in the diff. > > **Overview** > This **automatic release** finalizes **Android SDK 10.8.0** by replacing **`10.8.0-SNAPSHOT`** with **`10.8.0`** across versioning (`gradle.properties`, `.version`, `Config.frameworkVersion`), sample apps, and changelog files. > > Release notes for **10.8.0** are recorded in **`CHANGELOG.md`** / **`CHANGELOG.latest.md`** (workflows list API, paywall offering context on custom events, multipage paywall seam fix, rules-engine/JSON Logic work, etc.). **Docs publishing** now targets **`10.8.0`** on S3, and **`docs/index.html`** redirects to the new doc URL. > > There are **no functional code changes** in this diff beyond version strings and release metadata. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c3048b8. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->


Adds the domain model and serialization layer for workflow step lifecycle events. Wiring in
PaywallViewModelis in #3487.WorkflowEventsealed class (StepStarted,StepCompleted), sibling toPaywallEventBackendEvent.Workflowswire format matching khepri'sWorkflowsEventschemaBackendStoredEvent.Workflows+WorkflowEvent.toBackendStoredEvent()mapperBackendStoredEvent.Workflowsregistered inEventsManager's JSON polymorphic configDefinition in https://www.notion.so/revenuecat/Monetization-Event-Naming-Spec-32ccb1a108b0816d9a3efcf8f26813e4?source=copy_link
Note
Low Risk
Additive event-schema and plumbing following existing paywall/custom-paywall patterns; no auth or purchase logic changes, with coverage for JSON shape and persistence.
Overview
Introduces workflow step lifecycle analytics (
WorkflowEvent.StepStarted/StepCompleted) and wires them through the same path as paywall and customer-center events.Domain events map to a new
BackendEvent.Workflowspayload (khepri-compatible JSON withevent_name,properties, optionaltrace_id, etc.), persist asBackendStoredEvent.Workflows, and are registered inJsonProvider,EventsManagerpolymorphic JSON, andtrack()so they land in the event store and flush with other backend events. PaywallViewModel emission is explicitly out of scope (follow-up PR).Tests cover conversion, on-disk storage via
EventsManager, and request JSON shape (including fields that must not appear in output).Reviewed by Cursor Bugbot for commit 366a898. Bugbot is set up for automated code reviews on this repo. Configure here.