Skip to content

Add WorkflowEvent model and backend serialization#3486

Merged
vegaro merged 7 commits into
mainfrom
cesar/workflow-events-definition
May 28, 2026
Merged

Add WorkflowEvent model and backend serialization#3486
vegaro merged 7 commits into
mainfrom
cesar/workflow-events-definition

Conversation

@vegaro

@vegaro vegaro commented May 14, 2026

Copy link
Copy Markdown
Member

Adds the domain model and serialization layer for workflow step lifecycle events. Wiring in PaywallViewModel is in #3487.

  • WorkflowEvent sealed class (StepStarted, StepCompleted), sibling to PaywallEvent
  • BackendEvent.Workflows wire format matching khepri's WorkflowsEvent schema
  • BackendStoredEvent.Workflows + WorkflowEvent.toBackendStoredEvent() mapper
  • BackendStoredEvent.Workflows registered in EventsManager's JSON polymorphic config

Definition 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.Workflows payload (khepri-compatible JSON with event_name, properties, optional trace_id, etc.), persist as BackendStoredEvent.Workflows, and are registered in JsonProvider, EventsManager polymorphic JSON, and track() 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.

vegaro commented May 14, 2026

Copy link
Copy Markdown
Member Author

@vegaro vegaro changed the title Rebuild workflow step states on color scheme change (#3419) feat: add WorkflowEvent model and backend serialization May 14, 2026
@vegaro vegaro changed the title feat: add WorkflowEvent model and backend serialization Add WorkflowEvent model and backend serialization May 14, 2026
### 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>
@vegaro vegaro force-pushed the cesar/workflow-events-definition branch from be17d14 to 3f91e95 Compare May 14, 2026 16:30
@vegaro vegaro marked this pull request as ready for review May 14, 2026 16:30
@vegaro vegaro requested a review from a team as a code owner May 14, 2026 16:30
@OptIn(InternalRevenueCatAPI::class)
@JvmSynthetic
internal fun WorkflowEvent.toBackendStoredEvent(
appUserID: String,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it intentional to not have appSessionID? Every other event has it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, intentional. PaywallEvent has an appSessionID but not the workflow events.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this come from the backend? 🤔

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused by stepType + screenType

@vegaro vegaro May 28, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 facumenzella left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread purchases/src/main/kotlin/com/revenuecat/purchases/common/events/BackendEvent.kt Outdated
vegaro and others added 5 commits May 27, 2026 17:44
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

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.75000% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.96%. Comparing base (1690666) to head (366a898).

Files with missing lines Patch % Lines
...revenuecat/purchases/common/events/BackendEvent.kt 88.88% 0 Missing and 3 partials ⚠️
...purchases/common/workflows/events/WorkflowEvent.kt 82.35% 3 Missing ⚠️
...ecat/purchases/common/events/BackendStoredEvent.kt 93.54% 1 Missing and 1 partial ⚠️
...evenuecat/purchases/common/events/EventsManager.kt 75.00% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro vegaro added this pull request to the merge queue May 28, 2026
Merged via the queue into main with commit de44755 May 28, 2026
38 checks passed
@vegaro vegaro deleted the cesar/workflow-events-definition branch May 28, 2026 22:39
facumenzella added a commit to RevenueCat/purchases-ios that referenced this pull request May 29, 2026
#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>
matteinn pushed a commit to matteinn/purchases-android that referenced this pull request Jun 5, 2026
**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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants