-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[MOB-2759] Onboarding card NTP Experiment #778
Changes from 36 commits
f603f12
bd7565c
46241de
2ae78df
dd11dea
5efc188
a66a32b
cec5666
3745974
bd958f8
9028cdc
7984bcb
491f63b
dee519f
027e439
83adb87
8b2d65a
9c00e0b
7a046e7
1ffd26c
841c19f
5541802
e34f10b
0db6958
1ae7800
50f8f32
dec02b4
f57e4af
ce08512
a0031a3
808e637
57b0f5b
ac9ab67
4c1e311
fe78731
18e7623
c501aa5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,8 +29,27 @@ class LaunchCoordinator: BaseCoordinator, | |
func start(with launchType: LaunchType) { | ||
let isFullScreen = launchType.isFullScreenAvailable(isIphone: isIphone) | ||
switch launchType { | ||
case .intro(let manager): | ||
/* Ecosia: Change to support `OnboardingCardNTPExperiment` conditions | ||
case .intro(let manager): | ||
presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) | ||
*/ | ||
case .intro(let manager, let checkExperiment): | ||
guard checkExperiment else { | ||
presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) | ||
return | ||
} | ||
// TODO: Refactor `FeatureManagement.fetchConfiguration()` pre-condition - maybe a notification from FeatureManagement? | ||
Task { | ||
await FeatureManagement.fetchConfiguration() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering, what's seen on screen while this is being executed? Slightly relates to Luca's comment from line 52, but I would expect that at this point we have fetched the Unleash configuration already, so that we can make a synchronous decision. Don't we still have a LoadingScreen that's used when we get an invite code? We could bundle all async start up tasks there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with all the above and YES, it does require a refactor as we are all far from being happy about it 😅 . I tackled next steps already, creating a DRAFT RFC where I'm collecting info regarding the improvement. |
||
DispatchQueue.main.async { [weak self] in | ||
guard let self = self else { return } | ||
guard !OnboardingCardNTPExperiment.isEnabled else { | ||
self.parentCoordinator?.didFinishLaunch(from: self) | ||
return | ||
} | ||
self.presentIntroOnboarding(with: manager, isFullScreen: isFullScreen) | ||
} | ||
} | ||
Comment on lines
+41
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now this is working, but I don't think it looks great as it refreshes Unleash one extra time and mixes two concurrency patterns. Maybe this could be some sort of Notification sent by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to think about it thoroughly. For the time being, given the experiment context, I thought about leaving it as is. As a next step, I made a DRAFT RFC in our Confluence page that will be document with a proposed solution for us to comment on and eventually apply. |
||
case .update(let viewModel): | ||
presentUpdateOnboarding(with: viewModel, isFullScreen: isFullScreen) | ||
case .defaultBrowser: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,6 +82,13 @@ final class Analytics: AnalyticsProtocol { | |
.label(label.rawValue)) | ||
} | ||
|
||
func ntpOnboardingCardExperiment(_ action: Action) { | ||
track(Structured(category: Category.ntp.rawValue, | ||
action: action.rawValue) | ||
.label(Label.NTP.onboardingCard.rawValue) | ||
.property(OnboardingCardNTPExperiment.analyticsProperty)) | ||
} | ||
|
||
func navigation(_ action: Action, label: Label.Navigation) { | ||
track(Structured(category: Category.navigation.rawValue, | ||
action: action.rawValue) | ||
|
@@ -306,7 +313,7 @@ final class Analytics: AnalyticsProtocol { | |
guard let page else { | ||
return | ||
} | ||
let event = Structured(category: Category.intro.rawValue, | ||
let event = Structured(category: OnboardingCardNTPExperiment.analyticsIntroCategory ?? Category.intro.rawValue, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Matter of taste, but I am not really fond of this hidden fallback logic which relies on explicit knowledge in an allegedly agnostic function. Maybe we can have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree with that. I believe that was added for the simplicity of the experiment. |
||
action: Action.display.rawValue) | ||
.property(page.rawValue) | ||
.value(.init(integerLiteral: index)) | ||
|
@@ -317,7 +324,7 @@ final class Analytics: AnalyticsProtocol { | |
guard let page else { | ||
return | ||
} | ||
let event = Structured(category: Category.intro.rawValue, | ||
let event = Structured(category: OnboardingCardNTPExperiment.analyticsIntroCategory ?? Category.intro.rawValue, | ||
action: Action.click.rawValue) | ||
.label(label.rawValue) | ||
.property(page.rawValue) | ||
|
@@ -346,7 +353,7 @@ extension Analytics { | |
private func appendTestContextIfNeeded(_ action: Analytics.Action.Activity, _ event: Structured) { | ||
switch action { | ||
case .resume, .launch: | ||
addABTestContexts(to: event, toggles: [.searchShortcuts, .bingDistribution]) | ||
addABTestContexts(to: event, toggles: [.searchShortcuts, .bingDistribution, .onboardingCardNTP]) | ||
addCookieConsentContext(to: event) | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this | ||
// file, You can obtain one at http://mozilla.org/MPL/2.0/ | ||
|
||
import Foundation | ||
import Core | ||
|
||
struct OnboardingCardNTPExperiment { | ||
private enum Variant: String { | ||
case control | ||
case first = "test1" | ||
case second = "test2" | ||
} | ||
|
||
private init() {} | ||
|
||
static var isEnabled: Bool { | ||
Unleash.isEnabled(.onboardingCardNTP) && variant != .control | ||
} | ||
|
||
static private var variant: Variant { | ||
Variant(rawValue: Unleash.getVariant(.onboardingCardNTP).name) ?? .control | ||
} | ||
|
||
// MARK: Analytics | ||
static var analyticsIntroCategory: String? { | ||
isEnabled ? "intro_card" : nil | ||
} | ||
|
||
static var analyticsProperty: String? { | ||
switch variant { | ||
case .first: | ||
return "first_copy" | ||
case .second: | ||
return "second_copy" | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
/// Send onboarding card view analytics event, but just the first time it's called. | ||
static func trackExperimentImpression() { | ||
let trackExperimentImpressionKey = "onboardingCardNTPExperimentImpression" | ||
guard !UserDefaults.standard.bool(forKey: trackExperimentImpressionKey) else { | ||
return | ||
} | ||
Analytics.shared.ntpOnboardingCardExperiment(.view) | ||
UserDefaults.standard.setValue(true, forKey: trackExperimentImpressionKey) | ||
} | ||
|
||
// MARK: Card dismissed | ||
static private let cardDismissedKey = "onboardingCardNTPExperimentDismissed" | ||
|
||
static var shouldShowCard: Bool { | ||
isEnabled && !UserDefaults.standard.bool(forKey: cardDismissedKey) | ||
} | ||
|
||
static func setCardDismissed() { | ||
UserDefaults.standard.set(true, forKey: cardDismissedKey) | ||
} | ||
|
||
// MARK: Texts | ||
static var title: String { | ||
switch variant { | ||
case .first: | ||
return .localized(.onboardingCardNTPExperimentTitle1) | ||
case .second: | ||
return .localized(.onboardingCardNTPExperimentTitle2) | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
static var description: String { | ||
switch variant { | ||
case .first: | ||
return .localized(.onboardingCardNTPExperimentDescription1) | ||
case .second: | ||
return .localized(.onboardingCardNTPExperimentDescription2) | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
static var buttonTitle: String { | ||
switch variant { | ||
case .first: | ||
return .localized(.onboardingCardNTPExperimentButtonText1) | ||
case .second: | ||
return .localized(.onboardingCardNTPExperimentButtonText2) | ||
default: | ||
return "" | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a minor comment:
skipExperiment
already says what the method should do, no real reason to useforce
, or is there? ⚔️There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The real reason behind it. I guess it was added mostly to advocate the imperative instruction. Can update 👍