-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
[Playground] [Frontend] Migrate Playground to Google Analytics 4 #24957
Conversation
AnalyticsEvent and AnalyticsService in PGC, feedback split, GetIt instead of Provider
playground/frontend/lib/locator.dart
Outdated
@@ -57,3 +59,7 @@ void _initializeState() { | |||
), | |||
); | |||
} | |||
|
|||
void _initializeAnalyticsService() { |
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.
void _initializeAnalyticsService() { | |
void _initializeServices() { |
|
||
static AnalyticsService get(BuildContext context) { | ||
return Provider.of<AnalyticsService>(context, listen: false); | ||
abstract class AnalyticsService extends GeneralAnalyticsService { |
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.
How about this?
AnalyticsService -> PlaygroundAnalyticsService -> PlaygroundGoogleAnalyticsService
-> TobAnalyticsService -> TobGoogleAnalyticsService
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.
Simplified back to to AnalyticsService -> GoogleAnalyticsService.
playground/frontend/lib/locator.dart
Outdated
@@ -57,3 +59,7 @@ void _initializeState() { | |||
), | |||
); | |||
} | |||
|
|||
void _initializeAnalyticsService() { | |||
GetIt.instance.registerSingleton(GoogleAnalyticsService()); |
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.
You can register a single object under two handles:
GetIt.instance.registerSingleton(GoogleAnalyticsService()); | |
GetIt.instance.registerSingleton<AnalyticsService>(service); | |
GetIt.instance.registerSingleton<PlaygroundAnalyticsService>(service); |
This way playground_components
will request <AnalyticsService>
and be able to submit generic events, while Playground and ToB will fetch their own subclasses to submit their specific events.
AnalyticsService.get().trackClickSendPositiveFeedback( | ||
text, | ||
); | ||
} else { |
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.
Second thoughts, this is too much of investment into boolean. I suggest making an enum FeedbackRating
with positive
and negative
.
|
||
import 'package:get_it/get_it.dart'; | ||
import 'package:playground_components/playground_components.dart'; | ||
import 'package:usage/usage_html.dart'; |
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.
Is there a way to use a platform-agnostic implementation?
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 usage package will not be used, because it doesn't support GA4.
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.
Dropped usage
, manually submitting to GA4.
AnalyticsEvent( | ||
action: TobAnalyticsEvents.closeUnit, | ||
category: TobAnalyticsCategories.unit, | ||
label: '${sdk.title}_${unitId}_${timeSpent.inSeconds}s', |
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.
Is there a way to send raw property values for sdk.title
, unit.id
, and seconds to filter the events later?
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 usage package will not be used, because it doesn't support GA4.
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.
Dropped usage
, manually submitting to GA4.
Future<void> openUnit(Sdk sdk, UnitModel unit); | ||
Future<void> closeUnit(Sdk sdk, String unitId, Duration timeSpent); | ||
Future<void> completeUnit(Sdk sdk, UnitModel unit); | ||
// TODO(nausharipov): implement |
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.
?
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.
Deleted completeModule
after the discussion with Oleh.
@@ -57,6 +62,7 @@ class TourNotifier extends ChangeNotifier with PageStateMixin<void> { | |||
_appNotifier.addListener(_onAppNotifierChanged); | |||
_authNotifier.addListener(_onUnitProgressChanged); | |||
_onUnitChanged(); | |||
window.onBeforeUnload.listen(_onTabClosed); |
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.
Make a CloseNotifier
and do not import dart:html
here?
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.
Made WindowCloseNotifier
.
@@ -26,7 +26,8 @@ const cloudFunctionsBaseUrl = 'https://' | |||
|
|||
// Copied from Playground's config.g.dart | |||
|
|||
const String kAnalyticsUA = 'UA-73650088-2'; | |||
const String kAnalyticsUA = 'G-349612187'; |
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.
Does the usage
package work with the Analytics-4 new counters?
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 open issue indicates that it doesn't. I haven't found a wrapper package that supports GA4 other than firebase_analytics. Shall I use firebase_analytics?
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.
Dropped usage
, manually submitting to GA4.
|
||
class GenericAnalyticsEvents { | ||
static const run = 'click_run'; | ||
// TODO(nausharipov): rename example to snippet everywhere? |
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.
I suggest keeping it to not break the continuity of statistics.
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.
Renamed since we break continuity anyway with the new GA version.
|
||
@override | ||
AnalyticsEvent? lastSentEvent; | ||
|
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.
There should be more events common for the two projects. Cancelling comes to mind.
I suggest also adding feedback here. This way we make sure that both ToB and Playground handle the feedback the same way.
await AnalyticsService.get().trackRunExample( | ||
playgroundController.selectedExample!.name, | ||
); |
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.
Maybe not await most of the events? Except maybe feedback because the event is the business logic there.
* limitations under the License. | ||
*/ | ||
|
||
enum FeedbackRating { positive, negative } |
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.
Trailing comma, auto-format.
_setEnjoying(BuildContext context, bool isEnjoying) { | ||
return () { | ||
_getFeedbackState(context, false).setEnjoying(isEnjoying); | ||
AnalyticsService.get(context).trackClickEnjoyPlayground(isEnjoying); | ||
PlaygroundAnalyticsService.get().trackClickEnjoyPlayground(isEnjoying); |
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.
Use FeedbackRating
everywhere instead of bool?
playground/frontend/Dockerfile
Outdated
@@ -28,6 +28,7 @@ RUN apt-get update && apt-get install -y wget xz-utils git | |||
RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ | |||
tar -xf flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ | |||
mv flutter /opt/ &&\ | |||
git config --global --add safe.directory /opt/flutter &&\ |
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 recent version of Debian 11.6 has newer git
that by default does not allow operations outside of home directories. Flutter uses git
to download dependencies when it is ran for the first time, so this is required.
child: widget.createDropdown(_close), | ||
child: child, |
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.
chlid
had been creating multiple times here by OverlayEntry
builder leading to false positive analytics events.
Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment |
* tob event context (apache#25466) * flutter v3.7.3 upgrade (apache#25466) * BeamRunShortcut (apache#25466) * comment fixes (apache#25466) * tob app name for analytics parameter (apache#25466) * comment fixes (apache#25466) * the rest of tob events except for example modified (apache#25466) * questioned print (apache#25466) * additionalParams in SnippetModifiedAnalyticsEvent (apache#25466) * comment fixes & renamed events to past tense (apache#25466) * comment fixes (apache#25466) * reportIssueClicked (apache#25466) * sort BeamAnalyticsEvents (apache#25466) --------- Co-authored-by: darkhan.nausharipov <darkhan.nausharipov@kzn.akvelon.com>
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.
Change request is more for increased test coverage. Analytics is important as we assess the value of Beam Playground and Tour of Beam as our onboarding tool for the community.
Comments are more code design related and subjective. Otherwise, I was able to run locally and tests passed.
const String kAnalyticsUA = 'G-BXFP2FNCKC'; | ||
|
||
const String kApiClientURL = 'https://router.play-dev.beam.apache.org'; | ||
const String kApiJavaClientURL = 'https://java.play-dev.beam.apache.org'; | ||
const String kApiGoClientURL = 'https://go.play-dev.beam.apache.org'; | ||
const String kApiPythonClientURL = 'https://python.play-dev.beam.apache.org'; | ||
const String kApiScioClientURL = 'https://scio.play-dev.beam.apache.org'; |
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.
Code comment where these come from and how they are derived? Someone should read this and know how to change if needed.
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 two lines above it reads // Copied from Playground's config.g.dart
which in turn has an explanation.
config.g.dart
will be gone after merging #25610
If it happens before this merge, we will delete the ToB's config.dart
when merging master
here. Otherwise we will update #25610 accordingly if this is merged first.
@@ -61,3 +64,12 @@ void _initializeState() { | |||
), | |||
); | |||
} | |||
|
|||
void _initializeServices() { | |||
final googleAnalyticsService = GoogleAnalytics4Service( |
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.
What if the analytics service changes again from GoogleAnalytics4Service to something else?
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.
Also, is it possible to alias the library imports so someone knows where the class is from? So instead of SomeClass()
, we have library.SomeClass()
? Yes, in an IDE one could navigate to definition but many times people are reading code on GitHub directly; it's symbol lookup doesn't always work.
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.
- Renamed to
BeamAnalyticsService
andBeamGoogleAnalyticsSevice
. I like it better thenlibrary.Class
because the latter is rarely used, stands out, and questions why not import everything like that. And then we lose the Dart simplicity.
I. In that case we will make BeamSomethingElseAnalyticsService extends BeamAnalyticsService
, instantiate it here and register it in GetIt
under the same BeamAnalyticsService
interface. Renamed the variable to analyticsService
to further simplify the migration.
GetIt.instance.registerSingleton<AnalyticsService>( | ||
googleAnalyticsService, | ||
); |
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.
Instead of using GetIt why not just reference googleAnalyticsService? The project claims
As your App grows, at some point you will need to put your app's logic in classes that are separated from your Widgets. Keeping your widgets from having direct dependencies makes your code better organized and easier to test and maintain.
It isn't clear from this what is actually implementing the abstract AnalyticsService class without some digging. The promise to make the code better organized, at least in my attempt at reading the code, didn't seem to happen.
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.
Although the analytics classes are inside the shared playground_components
package, the instantiation of the service is external to it. This gives us the future flexibility to:
- Change the implementations for Playground and ToB independently.
- Write tests that mock the analytics service.
- Use the shared Playground widgets in a new app that does not send analytics data by registering a null-object for analytics.
There are a number of ways to implement this:
- Wrap Playground widgets in a
Provider
widget to provide them with a specific analytics service. But this gives no compile-time guarantee that the provider is in the tree and leads to bugs. - Use a service locator of which
get_it
is the most popular one. So we did.
It isn't clear from this what is actually implementing the abstract AnalyticsService class without some digging.
We need easy swap to a potential SomethingElseAnalyticsService
, so this abstraction is on purpose. We could register the service to be read as get<GoogleAnalyticsService>()
, but then swapping would affect dozens of locations.
The promise get_it
gives is that widgets no longer have to reference global variables or rely on Provider
s. And this promise is fulfilled. Provider
leads to even more digging while global variables bar the 3 opportunities above.
class TobEventContext with EquatableMixin { | ||
const TobEventContext({ | ||
required this.sdkId, | ||
required this.unitId, | ||
}); | ||
|
||
final String? sdkId; | ||
final String? unitId; | ||
|
||
static const empty = TobEventContext( | ||
sdkId: null, | ||
unitId: null, | ||
); |
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.
Could we spell out Tob to TourOfBeam and it isn't clear why it needs to implement EquatableMixin? Is it because there are two nullable properties and doing equals on this is challenging?
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.
- I would rather keep it
Tob
because we have many classes that need to emphasize their belonging to TourOfBeam. With this many classes we may assume the reader will soon be familiar withTob
. Some of the classes are long already. Some of them will become ambiguous if expanded, like isTourOfBeamMarkdown
a 'Tour of BeamMarkdown'?
- We need to compare objects in tests.
EqualityMixin
takes care of bothoperator ==
andhashCode
.
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
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.
I realize that the enum is obvious from its members but could we see a code comment describing its purpose which I'm assuming is in the context of Analytics events?
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.
+
@@ -38,7 +38,9 @@ class NewExampleAction extends StatelessWidget { | |||
label: 'intents.playground.newExample'.tr(), | |||
onPressed: () { | |||
launchUrl(Uri.parse('/')); | |||
AnalyticsService.get(context).trackClickNewExample(); | |||
PlaygroundComponents.analyticsService.sendUnawaited( |
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.
I agree with @Malarg here that sendNewExampleEvent makes sense instead of sendUnawaited.
import 'constants.dart'; | ||
|
||
/// Playground is loaded. | ||
class LoadedAnalyticsEvent extends AnalyticsEvent { |
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.
When you change this PR to add more tests, it would be nice to see this included. It looks like this and its base AnalyticsEvent class are just value classes without any exciting functionality so perhaps we can see coverage in the context of other analytics tests such as a test that loads the Playground and asserts that the LoadedAnalyticsEvent has the assigned sdk and/or snippet.
import 'constants.dart'; | ||
|
||
/// 'New Example' button is clicked in the app header. | ||
class NewExampleAnalyticsEvent extends AnalyticsEvent { |
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.
It would be great to see more test coverage for this. Like other extensions of AnalyticsEvent seemingly just data classes, it would be great to assert that the values are what is expected (I see the name property for example). In this context I would like to see a test mocking New Example button clicked and that the assertion passes that the name is what it should be.
} | ||
|
||
static AnalyticsService get() { | ||
return GetIt.instance.get<AnalyticsService>(); |
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.
I don't see how GetIt is making it clear where the actual implementation exists. I had to search quite a bit until I found the actual implementation. Nonetheless, if we decide to keep and use GetIt, can we make good use of it and expand our test coverage as the package promises to make easier?
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.
It does not because its goal is to decouple usage from implementation.
A reader familiar with GetIt would find-in-files 'GetIt.instance.register' to get to implementations.
As for swapping in mocks in tests, GetIt can do that for unit tests because there we can have different initialization. But we aim to not add new mocks to unit tests because we plan to migrate to another mocking library in the next phase.
Instead we test analytics with integration tests (since we anyway test everything by clicking through), and there we do not have the control of implementations because the real-app implementations are loaded. Thus we check BeamAnalyticsService.lastEvent
.
The only real benefit of GetIt that we use for now is to make playground_components
unaware of the implementation without using Provider and global variables, which justifies it to us.
} | ||
|
||
/// Sends [event] asynchronously without returning a [Future]. | ||
void sendUnawaited(AnalyticsEvent event) { |
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.
Why can't we just have Future<void> send(AnalyticsEvent event) async { ... }
?
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.
Because when we call it and discard the future, we get a discarded_future
lint. We would have to do one of the following with it:
- Disable the lint project-wide, which we do not want to, because it guards from logic errors.
- Disable it at each occurrence with which is harder to read.
…vigation (apache#24924) (#444) * Test analytics events for feedback, report issue, and external url navigation (apache#24924) * Test for run_started, run_finished, run_cancelled, loaded, sdk_selected, snippet_selected events (apache#24924) * Fix a run test (apache#24924) * Fix a test (apache#24924) * Fix a test (apache#24924) * Test for theme_set, new_example, and shortcuts analytics events (apache#24924) * Test code sharing (apache#24924) * Test for snippet_modified and snippet_reset events (apache#24924) * Clean up (apache#24924) * Clean up (apache#24924)
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.
@damccorm LGTM
Resolves #24924
Resolves #24901
Resolves #25466
Resolves #25489
Resolves #24591
This PR replaces the old Google Analytics with the new Google Analytics 4 in Playground. It is out of the Playground scope, but this is required for Tour of Beam, and it is easier to maintain only a single version of Analytics in both projects.
In this PR only Playground is migrated, and for ToB some TODOs are inserted. They will be resolved in
Some work on ToB is there because we initially wanted to work only on it and keep Playground intact, and it is easier to not cut that ToB work now.
Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:
addresses #123
), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, commentfixes #<ISSUE NUMBER>
instead.CHANGES.md
with noteworthy changes.See the Contributor Guide for more tips on how to make review process smoother.
To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md
GitHub Actions Tests Status (on master branch)
See CI.md for more information about GitHub Actions CI.