Skip to content

Commit

Permalink
Add loading bar performance experiment (#4978)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/488551667048375/1208210291365732/f

### Description

- Adds a custom experiment to measure the perceived performance of the
loading bar on existing users.
- The experiment randomly assigns a variant on-update, with percentage
roll-out controlled by the loadingBarExp feature toggle.

### Steps to test this PR
_Point at the config linked in the task_

-------
_In `LoadingBarExperimentVariantInitializer` set `val probabilities =
doubleArrayOf(0.0, 1.0)`_

_Enrolment_
- [x] Fresh install from this branch
- [x] Verify that `m_loading_bar_exp_enrollment_test` is sent

_Daily_
- [x] Verify that `m_browser_feature_daily_active_user_d` is sent with
param `loading_bar_exp=1`

_URI loaded_
- [x] Load a site (eg. example.com)
- [x] Verify that `m_uri_loaded` is sent with param `loading_bar_exp=1`

_Pull to refresh_
- [x] Pull to refresh the page
- [x] Verify that `m_browser_pull_to_refresh` is sent with param
`loading_bar_exp=1`
 
_Refresh_
- [x] Refresh the page from the menu
- [x] Verify that `m_nav_r_p` is sent with param `loading_bar_exp=1`

_Refresh action daily_
- [x] Verify that only one `m_refresh_action_daily` is sent with param
`loading_bar_exp=1`

_Loading bar_
- [x] Search for something on SERP
- [x] Scroll to hide the loading bar
- [x] Click a link
- [x] Verify that the loading bar shows immediately

_Feedback_
- [x] Go to “Settings” > “About” > “Share Feedback"
- [x] Share positive feedback
- [x] Verify that `mfbs_positive_submit` is sent with param
`loading_bar_exp=1`
- [x] Share negative feedback
- [x] Verify that `mfbs_negative_submit` is sent with param
`loading_bar_exp=1`

-------
_In `LoadingBarExperimentVariantInitializer` set `val probabilities =
doubleArrayOf(1.0, 0.0)`_

_Enrolment_
- [x] Fresh install from this branch
- [x] Verify that `m_loading_bar_exp_enrollment_control` is sent

_Daily_
- [x] Verify that `m_browser_feature_daily_active_user_d` is sent with
param `loading_bar_exp=0`

_URI loaded_
- [x] Load a site (eg. example.com)
- [x] Verify that `m_uri_loaded` is sent with param `loading_bar_exp=0`

_Pull to refresh_
- [x] Pull to refresh the page
- [x] Verify that `m_browser_pull_to_refresh` is sent with param
`loading_bar_exp=0`
 
_Refresh_
- [x] Refresh the page from the menu
- [x] Verify that `m_nav_r_p` is sent with param `loading_bar_exp=0`

_Refresh action daily_
- [x] Verify that only one `m_refresh_action_daily` is sent with param
`loading_bar_exp=0`

_Loading bar_
- [x] Search for something on SERP
- [x] Scroll to hide the loading bar
- [x] Click a link
- [x] Verify that there is a delay when showing the loading bar

_Feedback_
- [x] Go to “Settings” > “About” > “Share Feedback"
- [x] Share positive feedback
- [x] Verify that `mfbs_positive_submit` is sent with param
`loading_bar_exp=0`
- [x] Share negative feedback
- [x] Verify that `mfbs_negative_submit` is sent with param
`loading_bar_exp=0`

-------
_In the config, disable `allocateVariants`_

_Enrolment_
- [x] Fresh install from this branch
- [x] Verify that `m_loading_bar_exp_enrollment` pixel is **not** sent

_Daily_
- [x] Verify that `m_browser_feature_daily_active_user_d` is sent
**without** param `loading_bar_exp`

_URI loaded_
- [x] Load a site (eg. example.com)
- [x] Verify that `m_uri_loaded` is sent **without** param
`loading_bar_exp`

_Pull to refresh_
- [x] Pull to refresh the page
- [x] Verify that `m_browser_pull_to_refresh` is sent **without** param
`loading_bar_exp`
 
_Refresh_
- [x] Refresh the page from the menu
- [x] Verify that `m_nav_r_p` is sent **without** param
`loading_bar_exp`

_Refresh action daily_
- [x] Verify that only one `m_refresh_action_daily` is sent **without**
param `loading_bar_exp`

_Feedback_
- [x] Go to “Settings” > “About” > “Share Feedback"
- [x] Share positive feedback
- [x] Verify that `mfbs_positive_submit` is sent *without* param
`loading_bar_exp`
- [x] Share negative feedback
- [x] Verify that `mfbs_negative_submit` is sent *without* param
`loading_bar_exp`

-------
_In the config, re-enable `allocateVariants`_

- [x] Fresh install from this branch
- [x] Load a site (eg. example.com)
- [x] Verify that `m_uri_loaded` is sent with param `loading_bar_exp=0`

_In the config, disable the `loadingBarExp` feature_

- [x] Reload the config
- [x] Load a site (eg. example.com)
- [x] Verify that `m_uri_loaded` is sent **without** param
`loading_bar_exp`

### UI changes (Before/After)

https://github.com/user-attachments/assets/3ec616ed-4006-435c-86fd-fa55db2f1271
  • Loading branch information
joshliebe authored Sep 16, 2024
1 parent 65473a6 commit cee5de3
Show file tree
Hide file tree
Showing 24 changed files with 853 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import com.duckduckgo.app.browser.viewstate.FindInPageViewState
import com.duckduckgo.app.browser.viewstate.GlobalLayoutViewState
import com.duckduckgo.app.browser.viewstate.HighlightableButton
import com.duckduckgo.app.browser.viewstate.LoadingViewState
import com.duckduckgo.app.browser.viewstate.OmnibarViewState
import com.duckduckgo.app.browser.webview.SslWarningLayout.Action
import com.duckduckgo.app.cta.db.DismissedCtaDao
import com.duckduckgo.app.cta.model.CtaId
Expand Down Expand Up @@ -190,6 +191,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk
import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.feature.toggles.api.FeatureToggle
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.history.api.HistoryEntry.VisitedPage
Expand Down Expand Up @@ -375,6 +377,8 @@ class BrowserTabViewModelTest {

private val mockDuckDuckGoUrlDetector: DuckDuckGoUrlDetector = mock()

private var loadingBarExperimentManager: LoadingBarExperimentManager = mock()

private lateinit var remoteMessagingModel: RemoteMessagingModel

private val lazyFaviconManager = Lazy { mockFaviconManager }
Expand Down Expand Up @@ -613,6 +617,7 @@ class BrowserTabViewModelTest {
httpErrorPixels = { mockHttpErrorPixels },
duckPlayer = mockDuckPlayer,
duckPlayerJSHelper = DuckPlayerJSHelper(mockDuckPlayer, mockAppBuildConfig, mockPixel, mockDuckDuckGoUrlDetector),
loadingBarExperimentManager = loadingBarExperimentManager,
)

testee.loadData("abc", null, false, false)
Expand Down Expand Up @@ -5814,6 +5819,44 @@ class BrowserTabViewModelTest {
}
}

@Test
fun whenExperimentEnabledShowOmnibarImmediately() = runTest {
setBrowserShowing(true)
whenever(loadingBarExperimentManager.isExperimentEnabled()).thenReturn(true)
val observer = mock<(OmnibarViewState) -> Unit>()
testee.omnibarViewState.observeForever { observer(it) }

testee.navigationStateChanged(buildWebNavigation("https://example.com"))

val captor = argumentCaptor<OmnibarViewState>()
verify(observer, times(4)).invoke(captor.capture())

assertFalse(captor.allValues[0].navigationChange)
assertTrue(captor.allValues[1].navigationChange)
assertFalse(captor.allValues[2].navigationChange)
assertFalse(captor.allValues[3].navigationChange)

testee.omnibarViewState.removeObserver { observer(it) }
}

@Test
fun whenExperimentDisabledDoNotShowOmnibarImmediately() = runTest {
setBrowserShowing(true)
whenever(loadingBarExperimentManager.isExperimentEnabled()).thenReturn(false)
val observer = mock<(OmnibarViewState) -> Unit>()
testee.omnibarViewState.observeForever { observer(it) }

testee.navigationStateChanged(buildWebNavigation("https://example.com"))

val captor = argumentCaptor<OmnibarViewState>()
verify(observer, times(2)).invoke(captor.capture())

assertFalse(captor.allValues[0].navigationChange)
assertFalse(captor.allValues[1].navigationChange)

testee.omnibarViewState.removeObserver { observer(it) }
}

private fun aCredential(): LoginCredentials {
return LoginCredentials(domain = null, username = null, password = null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ import com.duckduckgo.app.browser.pageloadpixel.PageLoadedHandler
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedHandler
import com.duckduckgo.app.browser.print.PrintInjector
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.autofill.api.BrowserAutofill
import com.duckduckgo.autofill.api.InternalTestUserChecker
Expand All @@ -69,6 +71,7 @@ import com.duckduckgo.common.utils.device.DeviceInfo
import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.cookies.api.CookieManagerProvider
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
Expand Down Expand Up @@ -135,6 +138,7 @@ class BrowserWebViewClientTest {
private val subscriptions: Subscriptions = mock()
private val mockDuckPlayer: DuckPlayer = mock()
private val navigationHistory: NavigationHistory = mock()
private val loadingBarExperimentManager: LoadingBarExperimentManager = mock()

@UiThreadTest
@Before
Expand Down Expand Up @@ -168,6 +172,7 @@ class BrowserWebViewClientTest {
mediaPlayback,
subscriptions,
mockDuckPlayer,
loadingBarExperimentManager,
)
testee.webViewClientListener = listener
whenever(webResourceRequest.url).thenReturn(Uri.EMPTY)
Expand Down Expand Up @@ -929,6 +934,56 @@ class BrowserWebViewClientTest {
verify(listener).onReceivedSslError(any(), any())
}

@UiThreadTest
@Test
fun whenLoadingBarExperimentEnabledThenPixelFiredWithExperimentData() {
val mockWebView = getImmediatelyInvokedMockWebView()

whenever(loadingBarExperimentManager.isExperimentEnabled()).thenReturn(true)
whenever(loadingBarExperimentManager.variant).thenReturn(true)
whenever(mockWebView.settings).thenReturn(mock())
whenever(mockWebView.safeCopyBackForwardList()).thenReturn(TestBackForwardList())
whenever(mockWebView.progress).thenReturn(100)

testee.onPageFinished(mockWebView, EXAMPLE_URL)

verify(pixel).fire(
AppPixelName.URI_LOADED.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to "1"),
)
}

@UiThreadTest
@Test
fun whenLoadingBarExperimentDisabledThenPixelFiredWithoutExperimentData() {
val mockWebView = getImmediatelyInvokedMockWebView()

whenever(loadingBarExperimentManager.isExperimentEnabled()).thenReturn(false)
whenever(mockWebView.settings).thenReturn(mock())
whenever(mockWebView.safeCopyBackForwardList()).thenReturn(TestBackForwardList())
whenever(mockWebView.progress).thenReturn(100)

testee.onPageFinished(mockWebView, EXAMPLE_URL)

verify(pixel).fire(AppPixelName.URI_LOADED)
}

@UiThreadTest
@Test
fun whenLoadingBarExperimentEnabledButProgressIsNot100ThenNoPixelFired() {
val mockWebView = getImmediatelyInvokedMockWebView()

whenever(loadingBarExperimentManager.isExperimentEnabled()).thenReturn(true)
whenever(loadingBarExperimentManager.variant).thenReturn(true)
whenever(mockWebView.settings).thenReturn(mock())
whenever(mockWebView.safeCopyBackForwardList()).thenReturn(TestBackForwardList())
whenever(mockWebView.progress).thenReturn(50)

testee.onPageFinished(mockWebView, EXAMPLE_URL)

verify(pixel, never()).fire(anyString(), any(), any(), any())
}

private class TestWebView(context: Context) : WebView(context) {
override fun getOriginalUrl(): String {
return EXAMPLE_URL
Expand Down
44 changes: 41 additions & 3 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ import com.duckduckgo.app.privatesearch.PrivateSearchScreenNoParams
import com.duckduckgo.app.settings.db.SettingsDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.FIRE_BUTTON_STATE
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY
import com.duckduckgo.app.statistics.pixels.toBinaryString
import com.duckduckgo.app.tabs.model.TabEntity
import com.duckduckgo.app.tabs.ui.GridViewColumnCalculator
import com.duckduckgo.app.tabs.ui.TabSwitcherActivity
Expand Down Expand Up @@ -283,6 +286,7 @@ import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayerSettingsNoParams
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.js.messaging.api.JsMessageCallback
import com.duckduckgo.js.messaging.api.JsMessaging
Expand Down Expand Up @@ -545,6 +549,9 @@ class BrowserTabFragment :
@Inject
lateinit var duckPlayer: DuckPlayer

@Inject
lateinit var loadingBarExperimentManager: LoadingBarExperimentManager

/**
* We use this to monitor whether the user was seeing the in-context Email Protection signup prompt
* This is needed because the activity stack will be cleared if an external link is opened in our browser
Expand Down Expand Up @@ -2745,7 +2752,22 @@ class BrowserTabFragment :

binding.swipeRefreshContainer.setOnRefreshListener {
onRefreshRequested()
pixel.fire(AppPixelName.BROWSER_PULL_TO_REFRESH)

// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.BROWSER_PULL_TO_REFRESH.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = DAILY,
)
} else {
pixel.fire(AppPixelName.BROWSER_PULL_TO_REFRESH.pixelName)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName, type = DAILY)
}
}

binding.swipeRefreshContainer.setCanChildScrollUpCallback {
Expand Down Expand Up @@ -3582,7 +3604,21 @@ class BrowserTabFragment :
if (isActiveCustomTab()) {
pixel.fire(CustomTabPixelNames.CUSTOM_TABS_MENU_REFRESH)
} else {
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED.pixelName)
// Loading Bar Experiment
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.MENU_ACTION_REFRESH_PRESSED.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
pixel.fire(
AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
type = DAILY,
)
} else {
pixel.fire(AppPixelName.MENU_ACTION_REFRESH_PRESSED.pixelName)
pixel.fire(AppPixelName.REFRESH_ACTION_DAILY_PIXEL.pixelName, type = DAILY)
}
}
}
onMenuItemClicked(menuBinding.newTabMenuItem) {
Expand Down Expand Up @@ -3763,7 +3799,9 @@ class BrowserTabFragment :
cancelTrackersAnimation()
}

if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) {
if (viewState.navigationChange) {
omnibar.appBarLayout.setExpanded(true, true)
} else if (shouldUpdateOmnibarTextInput(viewState, viewState.omnibarText)) {
omnibar.omnibarTextInput.setText(viewState.omnibarText)
if (viewState.forceExpand) {
omnibar.appBarLayout.setExpanded(true, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ import com.duckduckgo.downloads.api.FileDownloader
import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.js.messaging.api.JsCallbackData
import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
Expand Down Expand Up @@ -279,6 +280,7 @@ class BrowserTabViewModel @Inject constructor(
private val httpErrorPixels: Lazy<HttpErrorPixels>,
private val duckPlayer: DuckPlayer,
private val duckPlayerJSHelper: DuckPlayerJSHelper,
private val loadingBarExperimentManager: LoadingBarExperimentManager,
) : WebViewClientListener,
EditSavedSiteListener,
DeleteBookmarkListener,
Expand Down Expand Up @@ -1154,6 +1156,10 @@ class BrowserTabViewModel @Inject constructor(

if (!currentBrowserViewState().browserShowing) return

if (loadingBarExperimentManager.isExperimentEnabled()) {
showOmniBar()
}

canAutofillSelectCredentialsDialogCanAutomaticallyShow = true

browserViewState.value = currentBrowserViewState().copy(
Expand Down Expand Up @@ -3449,6 +3455,15 @@ class BrowserTabViewModel @Inject constructor(
}
}

private fun showOmniBar() {
omnibarViewState.value = currentOmnibarViewState().copy(
navigationChange = true,
)
omnibarViewState.value = currentOmnibarViewState().copy(
navigationChange = false,
)
}

fun onUserDismissedAutoCompleteInAppMessage() {
viewModelScope.launch(dispatchers.io()) {
autoComplete.userDismissedHistoryInAutoCompleteIAM()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ import com.duckduckgo.app.browser.pageloadpixel.PageLoadedHandler
import com.duckduckgo.app.browser.pageloadpixel.firstpaint.PagePaintedHandler
import com.duckduckgo.app.browser.print.PrintInjector
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT
import com.duckduckgo.app.statistics.pixels.toBinaryString
import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.autofill.api.BrowserAutofill
import com.duckduckgo.autofill.api.InternalTestUserChecker
Expand All @@ -70,6 +73,7 @@ import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.cookies.api.CookieManagerProvider
import com.duckduckgo.duckplayer.api.DuckPlayer
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.history.api.NavigationHistory
import com.duckduckgo.privacy.config.api.AmpLinks
import com.duckduckgo.subscriptions.api.Subscriptions
Expand Down Expand Up @@ -109,6 +113,7 @@ class BrowserWebViewClient @Inject constructor(
private val mediaPlayback: MediaPlayback,
private val subscriptions: Subscriptions,
private val duckPlayer: DuckPlayer,
private val loadingBarExperimentManager: LoadingBarExperimentManager,
) : WebViewClient() {

var webViewClientListener: WebViewClientListener? = null
Expand Down Expand Up @@ -402,6 +407,14 @@ class BrowserWebViewClient @Inject constructor(
}
}
}
if (loadingBarExperimentManager.isExperimentEnabled()) {
pixel.fire(
AppPixelName.URI_LOADED.pixelName,
mapOf(LOADING_BAR_EXPERIMENT to loadingBarExperimentManager.variant.toBinaryString()),
)
} else {
pixel.fire(AppPixelName.URI_LOADED)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ data class OmnibarViewState(
val omnibarText: String = "",
val isEditing: Boolean = false,
val shouldMoveCaretToEnd: Boolean = false,
val navigationChange: Boolean = false,
val forceExpand: Boolean = true,
)
3 changes: 3 additions & 0 deletions app/src/main/java/com/duckduckgo/app/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.common.utils.plugins.pixel.PixelInterceptorPlugin
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.experiments.api.VariantManager
import com.duckduckgo.experiments.api.loadingbarexperiment.LoadingBarExperimentManager
import com.duckduckgo.user.agent.api.UserAgentProvider
import com.squareup.moshi.Moshi
import dagger.Lazy
Expand Down Expand Up @@ -171,6 +172,7 @@ class NetworkModule {
@AppCoroutineScope appCoroutineScope: CoroutineScope,
appBuildConfig: AppBuildConfig,
dispatcherProvider: DispatcherProvider,
loadingBarExperimentManager: LoadingBarExperimentManager,
): FeedbackSubmitter =
FireAndForgetFeedbackSubmitter(
feedbackService,
Expand All @@ -181,6 +183,7 @@ class NetworkModule {
appCoroutineScope,
appBuildConfig,
dispatcherProvider,
loadingBarExperimentManager,
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface FeedbackService {
@Field("manufacturer") manufacturer: String,
@Field("model") model: String,
@Field("atb") atb: String,
@Field("loading_bar_exp") loadingBarExperiment: String?,
)

companion object {
Expand Down
Loading

0 comments on commit cee5de3

Please sign in to comment.