Skip to content
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

Merged
merged 51 commits into from
Apr 12, 2023

Conversation

nausharipov
Copy link
Contributor

@nausharipov nausharipov commented Jan 10, 2023

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


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: 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, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

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)

Build python source distribution and wheels
Python tests
Java tests
Go tests

See CI.md for more information about GitHub Actions CI.

darkhan.nausharipov added 3 commits January 10, 2023 17:18
@@ -57,3 +59,7 @@ void _initializeState() {
),
);
}

void _initializeAnalyticsService() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
void _initializeAnalyticsService() {
void _initializeServices() {


static AnalyticsService get(BuildContext context) {
return Provider.of<AnalyticsService>(context, listen: false);
abstract class AnalyticsService extends GeneralAnalyticsService {
Copy link
Contributor

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

Copy link
Contributor

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.

@@ -57,3 +59,7 @@ void _initializeState() {
),
);
}

void _initializeAnalyticsService() {
GetIt.instance.registerSingleton(GoogleAnalyticsService());
Copy link
Contributor

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:

Suggested change
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 {
Copy link
Contributor

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';
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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',
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

?

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor

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';
Copy link
Contributor

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?

Copy link
Contributor Author

@nausharipov nausharipov Feb 2, 2023

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?

Copy link
Contributor

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?
Copy link
Contributor

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.

Copy link
Contributor

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;

Copy link
Contributor

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.

Comment on lines 54 to 56
await AnalyticsService.get().trackRunExample(
playgroundController.selectedExample!.name,
);
Copy link
Contributor

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 }
Copy link
Contributor

Choose a reason for hiding this comment

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

Trailing comma, auto-format.

Comment on lines 63 to 66
_setEnjoying(BuildContext context, bool isEnjoying) {
return () {
_getFeedbackState(context, false).setEnjoying(isEnjoying);
AnalyticsService.get(context).trackClickEnjoyPlayground(isEnjoying);
PlaygroundAnalyticsService.get().trackClickEnjoyPlayground(isEnjoying);
Copy link
Contributor

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?

@@ -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 &&\
Copy link
Contributor

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.

Comment on lines -157 to +160
child: widget.createDropdown(_close),
child: child,
Copy link
Contributor

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.

@nausharipov nausharipov changed the title [ToB] [Frontend] Tour of Beam analytics [Playground] [Frontend] Migrate Playground to Google Analytics 4 Feb 14, 2023
@nausharipov nausharipov marked this pull request as ready for review February 14, 2023 14:17
@github-actions
Copy link
Contributor

Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment assign set of reviewers

alexeyinkin and others added 5 commits February 22, 2023 16:49
* 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>
Copy link
Contributor

@damondouglas damondouglas left a 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.

Comment on lines 29 to 35
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';
Copy link
Contributor

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.

Copy link
Contributor

@alexeyinkin alexeyinkin Mar 9, 2023

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(
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

@alexeyinkin alexeyinkin Mar 9, 2023

Choose a reason for hiding this comment

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

  1. Renamed to BeamAnalyticsService and BeamGoogleAnalyticsSevice. I like it better then library.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.

Comment on lines 72 to 74
GetIt.instance.registerSingleton<AnalyticsService>(
googleAnalyticsService,
);
Copy link
Contributor

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.

Copy link
Contributor

@alexeyinkin alexeyinkin Mar 9, 2023

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:

  1. Change the implementations for Playground and ToB independently.
  2. Write tests that mock the analytics service.
  3. 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:

  1. 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.
  2. 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 Providers. And this promise is fulfilled. Provider leads to even more digging while global variables bar the 3 opportunities above.

Comment on lines 24 to 36
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,
);
Copy link
Contributor

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. 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 with Tob. Some of the classes are long already. Some of them will become ambiguous if expanded, like is TourOfBeamMarkdown a 'Tour of BeamMarkdown'?

image

  1. We need to compare objects in tests. EqualityMixin takes care of both operator == and hashCode.

* See the License for the specific language governing permissions and
* limitations under the License.
*/

Copy link
Contributor

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?

Copy link
Contributor

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(
Copy link
Contributor

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 {
Copy link
Contributor

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 {
Copy link
Contributor

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>();
Copy link
Contributor

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?

Copy link
Contributor

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) {
Copy link
Contributor

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 { ... }?

Copy link
Contributor

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.

alexeyinkin and others added 8 commits March 9, 2023 17:58
…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)
Copy link
Contributor

@damondouglas damondouglas left a comment

Choose a reason for hiding this comment

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

@damccorm LGTM

@damccorm damccorm merged commit 2597ca1 into apache:master Apr 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
5 participants