Skip to content

CMM-1990: Add UTM stats card to new Stats Traffic tab#22772

Merged
adalpari merged 21 commits intotrunkfrom
feat/CMM-1990-stats-add-UTM
Apr 9, 2026
Merged

CMM-1990: Add UTM stats card to new Stats Traffic tab#22772
adalpari merged 21 commits intotrunkfrom
feat/CMM-1990-stats-add-UTM

Conversation

@adalpari
Copy link
Copy Markdown
Contributor

@adalpari adalpari commented Apr 7, 2026

Description

Note: Danger is failing because of the code targeting a worpdress-rs PR instead of trunk

Adds a new UTM stats card to the Traffic tab in the new Stats screen, allowing users to view UTM-tagged traffic data broken down by category (Source/Medium, Campaign/Source/Medium, Source, Medium, Campaign).

Key changes:

  • New UtmCard Compose component with category dropdown selector
  • UtmDetailActivity with its own ViewModel that independently fetches data (avoids Intent size limits)
  • Data layer: StatsDataSource.fetchUtm()StatsRepository.fetchUtm() with comparison period support
  • Per-category state caching with ConcurrentHashMap for thread safety
  • Expandable rows showing top posts per UTM value
  • Category preference persistence via AppPrefsWrapper
  • StatsPeriod serialization helpers (toTypeString/fromTypeString) for passing period info between screens
  • Unit tests for UtmViewModel covering name formatting, error states, item capping, and top posts

Also includes (from trunk):

  • Reader: Scope photo viewer swiping to gallery images
  • Reader: Move Subscribe button to bottom row, remove View Original
  • Reader: Fix post detail content jumping (CMM-2012)
  • PostRs: Add analytics tracking to post list and settings

Testing instructions

UTM card on Traffic tab:

Note: Fieldguide stats contain UTM data is you select 6 or 12 months period

  1. Open the new Stats screen → Traffic tab
  2. Scroll to the UTM card
  • Verify the card shows loading shimmer, then data or empty state
  1. Tap the category dropdown and switch between categories
  • Verify each category loads independently and selection is preserved across sessions
  1. Tap "Show all" footer
  • Verify the detail screen opens, shows loading state, then fetches and displays all items
Screenshot 2026-04-07 at 17 31 32

adalpari and others added 12 commits April 7, 2026 11:48
Add a new UTM card that displays UTM tracking data with a dropdown
category selector (Source/Medium, Campaign/Source/Medium, Source,
Medium, Campaign), horizontal bar items with view counts, and a
"Show all" detail screen. The data layer is stubbed pending
wordpress-rs UTM API bindings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update wordpress-rs to 1253-2cdcba95 which includes UTM UniFFI
bindings, and replace the stubbed fetchUtm with a real implementation
using StatsUtmParams and the statsUtm() request executor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show the detail screen CTA unconditionally when items are present,
matching the Authors card behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stop clearing loadingPeriod in coroutine finally blocks so the
onPeriodChanged guard correctly prevents duplicate fetches when
LaunchedEffect re-fires. Affects Authors, UTM, MostViewed, and
BaseStatsCard (Clicks, SearchTerms, VideoPlays, FileDownloads).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d tests (CMM-1990)

Remove unused change fields from UTM UI models and detail activity,
make loadingPeriod per-category to prevent races, use category label
as dynamic list header, and add UtmViewModelTest with 8 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…il screen (CMM-1990)

Replace mutable maps with ConcurrentHashMap in UtmViewModel for thread-safe
access. Refactor UtmDetailActivity to fetch data via its own ViewModel instead
of receiving all data through Intent extras, avoiding TransactionTooLargeException
risk. Add StatsPeriod serialization helpers for passing period info between screens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nTest (CMM-1990)

Restore try/finally blocks that clear loadingPeriod after fetch completes
in Authors, BaseStatsCard, MostViewed, and UTM ViewModels, reverting the
changes from 15702a3. Also add UTM to the expected hidden cards list
in StatsCardsConfigurationTest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract helper methods to reduce method length in StatsRepository.fetchUtm
and UtmDetailViewModel.fetchData. Suppress SpreadOperator and ReturnCount
warnings where alternatives are not practical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Apr 7, 2026

2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 7, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: wordpressRelease):

--- ./build/reports/diff_manifest/WordPress/wordpressRelease/base_manifest.txt	2026-04-09 09:01:32.121855256 +0000
+++ ./build/reports/diff_manifest/WordPress/wordpressRelease/head_manifest.txt	2026-04-09 09:01:40.981854950 +0000
@@ -205,6 +205,10 @@
             android:exported="false"
             android:theme="@style/WordPress.NoActionBar" />
         <activity
+            android:name="org.wordpress.android.ui.newstats.utm.UtmDetailActivity"
+            android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
             android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
             android:exported="false"
             android:theme="@style/WordPress.NoActionBar" />

Go to https://buildkite.com/automattic/wordpress-android/builds/25952/canvas?sid=019d7175-3b6c-423a-ab05-836d3e77671a, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 7, 2026

Project dependencies changes

list
! Upgraded Dependencies
rs.wordpress.api:android:trunk-0d94794142482d1b7f9395c0afef57ac991c452e, (changed from trunk-262a778ead5f163f3450d62adfac21fb32048714)
rs.wordpress.api:kotlin:trunk-0d94794142482d1b7f9395c0afef57ac991c452e, (changed from trunk-262a778ead5f163f3450d62adfac21fb32048714)
tree
 +--- project :libs:fluxc
-|    \--- rs.wordpress.api:android:trunk-262a778ead5f163f3450d62adfac21fb32048714
-|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okio:okio:3.16.4 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.20 (*)
-|         +--- net.java.dev.jna:jna:5.18.1
-|         +--- rs.wordpress.api:kotlin:trunk-262a778ead5f163f3450d62adfac21fb32048714
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
-|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.20 (*)
-|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.20 (*)
+|    \--- rs.wordpress.api:android:trunk-0d94794142482d1b7f9395c0afef57ac991c452e
+|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okio:okio:3.16.4 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.20 (*)
+|         +--- net.java.dev.jna:jna:5.18.1
+|         +--- rs.wordpress.api:kotlin:trunk-0d94794142482d1b7f9395c0afef57ac991c452e
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
+|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.20 (*)
+|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.20 (*)
-\--- rs.wordpress.api:android:trunk-262a778ead5f163f3450d62adfac21fb32048714 (*)
+\--- rs.wordpress.api:android:trunk-0d94794142482d1b7f9395c0afef57ac991c452e (*)

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 7, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: jetpackRelease):

--- ./build/reports/diff_manifest/WordPress/jetpackRelease/base_manifest.txt	2026-04-09 09:01:36.379032386 +0000
+++ ./build/reports/diff_manifest/WordPress/jetpackRelease/head_manifest.txt	2026-04-09 09:01:43.959040987 +0000
@@ -393,6 +393,10 @@
             android:exported="false"
             android:theme="@style/WordPress.NoActionBar" />
         <activity
+            android:name="org.wordpress.android.ui.newstats.utm.UtmDetailActivity"
+            android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
             android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
             android:exported="false"
             android:theme="@style/WordPress.NoActionBar" />

Go to https://buildkite.com/automattic/wordpress-android/builds/25952/canvas?sid=019d7175-3b6d-4b81-b186-167c80c50eae, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 7, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22772-3ffe93c
Build Number1488
Application IDorg.wordpress.android.prealpha
Commit3ffe93c
Installation URL2foe11qkr5350
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Apr 7, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22772-3ffe93c
Build Number1488
Application IDcom.jetpack.android.prealpha
Commit3ffe93c
Installation URL7tjstahure4ho
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Apr 7, 2026

@claude

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 7, 2026

Claude finished @adalpari's task in 3m 16s —— View job


Code Review

  • Read and analyze changed files
  • Review ViewModel layer (UtmViewModel, UtmDetailViewModel)
  • Review UI layer (UtmCard, UtmDetailActivity, UtmCardUiState)
  • Review data layer (StatsDataSource, StatsRepository)
  • Review test coverage (UtmViewModelTest)
  • Review supporting changes (StatsPeriod, AppPrefs, StatsCardType)
  • Post final review

Overall this is a solid, well-structured feature. The architecture follows existing patterns (per-category state caching, ConcurrentHashMap for thread-safety, coroutine-based fetching), and the UI follows the established stats card conventions. A few issues worth addressing before merging:


Bugs

1. loadingPeriods is never populated — in-flight dedup guard is dead code

UtmViewModel.kt:153-155

The loadingPeriods map is intended to guard against duplicate concurrent fetches while a request is already in-flight. However, a value is never inserted into the map — only removed (in the finally block of loadData() and inside fetchForCategory). This means:

  • loadingPeriods[cat] == period in onPeriodChanged is always null, so the guard never triggers.
  • The finally { loadingPeriods.remove(cat) } in loadData() does nothing (the entry was never inserted).
  • Rapid period changes while a fetch is in-flight can fire multiple concurrent requests for the same category.

The fix is to add loadingPeriods[cat] = currentPeriod before launching the coroutine in loadData(), and similarly in onCategoryChanged. Fix this →


Code Quality Issues

2. Fragile positional access in uiState combine

UtmViewModel.kt:47-60

The combine lambda uses values[0] as UtmCategory and states[cat.ordinal] — coupling this to the ordinal ordering of UtmCategory.entries. If entries are reordered or a new entry is inserted at the wrong position, states[cat.ordinal] will silently return the wrong state. The @Suppress("UNCHECKED_CAST") is a hint that this is fragile.

A safer approach is to look up the state directly from the map after the combine fires:

val uiState: StateFlow<UtmCardUiState> = combine(
    _selectedCategory,
    *_categoryStates.values.toTypedArray()
) { _ ->  // ignore the array, react to any change
    _categoryStates[_selectedCategory.value]?.value
        ?: UtmCardUiState.Loading
}.stateIn(...)

Or simply combine _selectedCategory with the specific flow for the selected category using flatMapLatest.

3. formatUtmName uses fragile manual JSON parsing

UtmCardUiState.kt:99-107

The function splits on , to parse what is effectively a JSON array. This breaks silently if any UTM value contains a comma (e.g. ["source,comma","medium"]source,comma / medium becomes source + comma" / "medium). Since UTM values in the wild can contain commas, using a proper JSON parser (e.g. org.json.JSONArray already available in Android) would be safer:

internal fun formatUtmName(raw: String): String {
    if (!raw.startsWith("[")) return raw
    return try {
        val arr = org.json.JSONArray(raw)
        (0 until arr.length()).joinToString(" / ") { arr.getString(it) }
    } catch (_: Exception) {
        raw
    }
}

4. Auth error in detail screen silently drops "Open WP Admin" action

UtmDetailActivity.kt:163-176

StatsCardErrorContent is called without onOpenWpAdmin, so an auth error in the detail screen shows no recovery path, unlike the card on the Traffic tab which does show the WP Admin link. UtmDetailViewModel.getAdminUrl() is already defined but never wired up in the activity's error state. Consider passing:

onOpenWpAdmin = if (uiState.isAuthError) {
    { viewModel.getAdminUrl()?.let { openWpAdmin(it, context) } }
} else null

5. UtmDetailViewModel.getAdminUrl() appears unused

UtmDetailViewModel.kt:77-79

getAdminUrl() is defined on the ViewModel but UtmDetailActivity never calls it (see issue #4 above). Either wire it up or remove the dead method.


Testing

6. No tests for UtmDetailViewModel (also flagged by Danger CI)

UtmDetailViewModel.kt

The detail ViewModel contains non-trivial logic: resolveCategory() and resolvePeriod() (including the Custom period fallback when epoch days are missing), buildLoadedState(), error state propagation, and the hasLoaded one-shot guard. These should have unit tests similar to UtmViewModelTest. Fix this →


Minor Observations

  • StatsPeriod.fromTypeString silent fallback (StatsPeriod.kt:73): Unknown type strings fall back to Last7Days with no logging. A AppLog.w(...) at the else branch would make debugging easier if a new period type is ever mismatched.
  • Expand/collapse icon accessibility (UtmCard.kt:348, UtmDetailActivity.kt:349): The expand arrow icon has contentDescription = null. Since the parent row already handles the click, this is reasonable, but talkback users won't know the row is expandable. Consider a content description like stringResource(if (expanded) R.string.collapse else R.string.expand) on the icon.
  • UtmCardUiState and UtmDetailUiState in same file: Having both state hierarchies plus helper types (UtmUiItem, UtmPostUiItem, UtmCategory, formatUtmName) in one file makes UtmCardUiState.kt do a lot of work. No action needed, just worth considering if this grows further.

adalpari and others added 5 commits April 7, 2026 16:43
The loadingPeriods map was never written to, making the duplicate
fetch guard in onPeriodChanged dead code. Add the assignment before
launching coroutines in loadData() and onCategoryChanged() so rapid
period changes are correctly deduplicated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fragile positional combine with flatMapLatest for safe
category state lookup. Use Gson JsonParser instead of manual string
splitting to correctly handle commas in UTM values. Wire up Open WP
Admin action in detail screen error state for auth error recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hange race (CMM-1990)

Extract shared UtmExpandableRow/UtmPostRow into UtmCommonComposables, replace
StatsCardErrorContent with a lightweight DetailErrorContent on the detail screen,
remove unused per-item change properties from UtmItemData, cancel in-flight fetch
jobs on period change to prevent stale data, and track UtmDetailViewModelTest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…(CMM-1990)

Drop the previous-period API call and change percentage from UTM stats
since this card does not need period diffs. Also add proportional bar
backgrounds to expanded top-post rows, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari adalpari marked this pull request as ready for review April 7, 2026 16:13
@adalpari adalpari requested a review from a team as a code owner April 7, 2026 16:13
@adalpari adalpari requested review from nbradbury and removed request for a team April 7, 2026 16:13
@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari The UTM card isn't shown by default - I have to add it. Is this intentional?

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Apr 8, 2026

@adalpari The UTM card isn't shown by default - I have to add it. Is this intentional?

Good catch. I didn't think about this tbh (it's a premium card), and because there are so many cards, maybe we should restrict the number of the default ones.

I'll open a new PR selecting those cards by default, and maybe showing the premium ones depending on the account type. So, not a blocker for this PR. (I'm adding it to the default list anyway)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari The use a of button to show a menu seems off to me, especially since the button style is identical to the toggle button style used in other cards. Maybe a dropdown with an arrow would work better here?

stats1

Swap the OutlinedButton for a plain text label with a dropdown arrow
icon so the category picker is visually distinct from toggle buttons
used in other stats cards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Apr 8, 2026

@adalpari The use a of button to show a menu seems off to me, especially since the button style is identical to the toggle button style used in other cards. Maybe a dropdown with an arrow would work better here?

Sounds reasonable. Done!

Screenshot 2026-04-08 at 17 02 32

@nbradbury
Copy link
Copy Markdown
Contributor

@adalpari I'm unable to test this with the Fieldguide due to a login issue. Could you privately send a video of this in action with real data? In the meantime, Claude had some suggestions:

  2. formatUtmName uses bare Exception catch (Low)

  UtmCardUiState.kt:122 — The catch (_: Exception) in formatUtmName will silently swallow any exception from Gson parsing. Consider catching
  JsonParseException specifically, or at minimum logging the error so malformed API data doesn't go unnoticed.

  3. StatsRepository.fetchUtm calculates _ (comparison range) but never uses it (Low)

  StatsRepository.kt line in fetchUtm:
  val (curRange, _) = calculateComparisonDateRanges(period)
  The previous-period range is calculated but discarded. This is correct since comparison was intentionally removed, but it's wasteful — consider using a
  helper that only calculates the current range to avoid computing dates that are thrown away.

  4. onPeriodChanged clears ALL loaded periods (Medium)

  UtmViewModel.kt:onPeriodChanged calls loadedPeriods.clear() and loadingPeriods.clear() on any period change. This means switching periods and switching
  back will always re-fetch for all categories, even though the data for the previous period was already loaded. This is probably fine for correctness, but
  worth noting if caching across periods is desired later.

adalpari and others added 2 commits April 9, 2026 10:29
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Narrow bare Exception catch to JsonParseException in formatUtmName,
use calculateCurrentDateRange instead of unused comparison range in
StatsRepository, and adapt SiteCapabilityChecker and SampleUsers to
the new wordpress-rs UserCapabilitiesMap type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Apr 9, 2026

@adalpari I'm unable to test this with the Fieldguide due to a login issue. Could you privately send a video of this in action with real data? In the meantime, Claude had some suggestions:

Thank you. I've made some changes and sent you a video.

About this point:

  1. onPeriodChanged clears ALL loaded periods (Medium)

I saw it, but I do believe it's better to actually clear loaded period to avoid outdated or corrupted data.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 25.17401% with 645 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.33%. Comparing base (2eec23e) to head (3ffe93c).
⚠️ Report is 5 commits behind head on trunk.

Files with missing lines Patch % Lines
...a/org/wordpress/android/ui/newstats/utm/UtmCard.kt 0.00% 167 Missing ⚠️
...press/android/ui/newstats/utm/UtmDetailActivity.kt 0.00% 118 Missing ⚠️
...ss/android/ui/newstats/utm/UtmCommonComposables.kt 0.00% 105 Missing ⚠️
.../wordpress/android/ui/newstats/utm/UtmViewModel.kt 55.78% 53 Missing and 12 partials ⚠️
.../android/ui/newstats/repository/StatsRepository.kt 20.63% 49 Missing and 1 partial ⚠️
...roid/ui/newstats/datasource/StatsDataSourceImpl.kt 0.00% 42 Missing ⚠️
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 37 Missing ⚠️
...a/org/wordpress/android/ui/newstats/StatsPeriod.kt 4.16% 22 Missing and 1 partial ⚠️
.../android/ui/newstats/datasource/StatsDataSource.kt 0.00% 12 Missing ⚠️
...ress/android/ui/newstats/utm/UtmDetailViewModel.kt 86.25% 7 Missing and 4 partials ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22772      +/-   ##
==========================================
- Coverage   37.41%   37.33%   -0.09%     
==========================================
  Files        2321     2327       +6     
  Lines      123775   124627     +852     
  Branches    16804    16883      +79     
==========================================
+ Hits        46312    46527     +215     
- Misses      73747    74366     +619     
- Partials     3716     3734      +18     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

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

Thanks for the video, looks good! :shipit:

@adalpari adalpari merged commit 4ecd415 into trunk Apr 9, 2026
22 checks passed
@adalpari adalpari deleted the feat/CMM-1990-stats-add-UTM branch April 9, 2026 10:40
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.

4 participants