Skip to content

Commit

Permalink
feat: Added core usage logging (#1147)
Browse files Browse the repository at this point in the history
* feat: Initial tracking structure

* Added testing page

* Created track constructor

* Deleted unneccassary _trackConstructor params

* Finished start track

* Fix id + country

* Finished: trackScannedProduct

* Finished: trackProductPageOpen

* Finished: trackKnowledgePanelOpen

* Finished: trackOpenLink

* Finished: trackPersonalizedRanking

* Finished: trackSearch

* Added documentation + fixed imports

* Code review

* trackKnowledgePanelOpen

* Code review

* Update main.dart
  • Loading branch information
M123-dev authored Feb 18, 2022
1 parent bc41ba8 commit 7783e41
Show file tree
Hide file tree
Showing 15 changed files with 423 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:smooth_app/cards/product_cards/knowledge_panels/knowledge_panel_expanded_card.dart';
import 'package:smooth_app/cards/product_cards/knowledge_panels/knowledge_panel_summary_card.dart';
import 'package:smooth_app/generic_lib/widgets/smooth_card.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';
import 'package:smooth_app/helpers/ui_helpers.dart';
import 'package:smooth_app/themes/smooth_theme.dart';
import 'package:smooth_app/themes/theme_provider.dart';
Expand All @@ -31,31 +32,36 @@ class KnowledgePanelCard extends StatelessWidget {
}
return InkWell(
child: KnowledgePanelSummaryCard(panel),
onTap: () => Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => Scaffold(
backgroundColor: SmoothTheme.getColor(
themeData.colorScheme,
SmoothTheme.getMaterialColor(themeProvider),
ColorDestination.SURFACE_BACKGROUND,
),
appBar: AppBar(),
body: SingleChildScrollView(
child: SmoothCard(
color: const Color(0xfff5f6fa),
padding: const EdgeInsets.all(
SMALL_SPACE,
),
child: KnowledgePanelExpandedCard(
panel: panel,
allPanels: allPanels,
onTap: () {
AnalyticsHelper.trackKnowledgePanelOpen(
knowledgePanelName: panel.topics.toString(),
);
Navigator.push<Widget>(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => Scaffold(
backgroundColor: SmoothTheme.getColor(
themeData.colorScheme,
SmoothTheme.getMaterialColor(themeProvider),
ColorDestination.SURFACE_BACKGROUND,
),
appBar: AppBar(),
body: SingleChildScrollView(
child: SmoothCard(
color: const Color(0xfff5f6fa),
padding: const EdgeInsets.all(
SMALL_SPACE,
),
child: KnowledgePanelExpandedCard(
panel: panel,
allPanels: allPanels,
),
),
),
),
),
),
),
);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:smooth_app/database/barcode_product_query.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/helpers/analytics_helper.dart';

enum ScannedProductState {
FOUND,
Expand Down Expand Up @@ -99,6 +100,8 @@ class ContinuousScanModel with ChangeNotifier {
if (_latestScannedBarcode == code) {
return;
}
AnalyticsHelper.trackScannedProduct(barcode: code);

_latestScannedBarcode = code;
_addBarcode(code);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/smooth_app/lib/data_models/product_query_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ProductQueryModel with ChangeNotifier {
final ProductListSupplier supplier;

static const String _CATEGORY_ALL = 'all';
String currentCategory = _CATEGORY_ALL;

LoadingStatus _loadingStatus = LoadingStatus.LOADING;
String? _loadingError;
Expand Down Expand Up @@ -108,6 +109,7 @@ class ProductQueryModel with ChangeNotifier {
}

void selectCategory(String category) {
currentCategory = category;
if (category == _CATEGORY_ALL) {
displayProducts = _products;
} else {
Expand Down
23 changes: 23 additions & 0 deletions packages/smooth_app/lib/database/dao_int.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:smooth_app/database/abstract_dao.dart';
import 'package:smooth_app/database/local_database.dart';

/// Where we store ints.
class DaoInt extends AbstractDao {
DaoInt(final LocalDatabase localDatabase) : super(localDatabase);

static const String _hiveBoxName = 'int';

@override
Future<void> init() async => Hive.openBox<int>(_hiveBoxName);

@override
void registerAdapter() {}

Box<int> _getBox() => Hive.box<int>(_hiveBoxName);

int? get(final String key) => _getBox().get(key);

Future<void> put(final String key, final int? value) async =>
value == null ? _getBox().delete(key) : _getBox().put(key, value);
}
2 changes: 2 additions & 0 deletions packages/smooth_app/lib/database/local_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:smooth_app/database/abstract_dao.dart';
import 'package:smooth_app/database/dao_int.dart';
import 'package:smooth_app/database/dao_product.dart';
import 'package:smooth_app/database/dao_product_list.dart';
import 'package:smooth_app/database/dao_string.dart';
Expand All @@ -26,6 +27,7 @@ class LocalDatabase extends ChangeNotifier {
DaoProductList(localDatabase),
DaoStringList(localDatabase),
DaoString(localDatabase),
DaoInt(localDatabase),
];
for (final AbstractDao dao in daos) {
dao.registerAdapter();
Expand Down
225 changes: 225 additions & 0 deletions packages/smooth_app/lib/helpers/analytics_helper.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import 'dart:async';
import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:matomo_forever/matomo_forever.dart';
import 'package:openfoodfacts/model/Product.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:smooth_app/database/local_database.dart';
import 'package:smooth_app/helpers/tracking_database_helper.dart';

// TODO(m123): Check for user consent

/// Helper for logging usage of core features and exceptions
/// Logging:
/// - Errors and Problems (sentry)
/// - App start
/// - Product scan
/// - Product page open
/// - Knowledge panel open
/// - personalized ranking (without sharing the preferences)
/// - search
/// - external links
class AnalyticsHelper {
AnalyticsHelper._();

static const String _initAction = 'started app';
static const String _scanAction = 'scanned product';
static const String _productPageAction = 'opened product page';
static const String _knowledgePanelAction = 'opened knowledge panel page';
static const String _personalizedRankingAction = 'personalized ranking';
static const String _searchAction = 'search';
static const String _linkAction = 'opened link';

/// The event category. Must not be empty. (eg. Videos, Music, Games...)
static const String _eventCategory = 'e_c';

/// Must not be empty. (eg. Play, Pause, Duration, Add
/// Playlist, Downloaded, Clicked...)
static const String _eventAction = 'e_a';

/// The event name. (eg. a Movie name, or Song name, or File name...)
static const String _eventName = 'e_n';

/// Must be a float or integer value (numeric), not a string.
static const String _eventValue = 'e_v';

static String latestSearch = '';

static Future<void> initSentry({Function()? appRunner}) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();

await SentryFlutter.init(
(SentryOptions options) {
options.dsn =
'https://[email protected]/5376745';
options.sentryClientName =
'sentry.dart.smoothie/${packageInfo.version}';
},
appRunner: appRunner,
);
}

static Future<void> initMatomo(
final BuildContext context,
final LocalDatabase _localDatabase,
) async {
MatomoForever.init(
'https://analytics.openfoodfacts.org/matomo.php',
2,
id: _getId(),
// If we track or not, should be decidable later
rec: true,
method: MatomoForeverMethod.post,
sendImage: false,
// 32 character authorization key used to authenticate the API request
// only needed for request which are more then 24h old
// tokenAuth: 'xxx',
);
}

static Future<bool> trackStart(
LocalDatabase _localDatabase, BuildContext context) async {
final TrackingDatabaseHelper trackingDatabaseHelper =
TrackingDatabaseHelper(_localDatabase);
final Size size = MediaQuery.of(context).size;
final Map<String, String> data = <String, String>{};

// The current count of visits for this visitor
data.addIfVAndNew(
'_idvc', trackingDatabaseHelper.getAppVisits().toString());
// The UNIX timestamp of this visitor's previous visit
data.addIfVAndNew(
'_viewts',
trackingDatabaseHelper.getPreviousVisitUnix().toString(),
);
// The UNIX timestamp of this visitor's first visit
data.addIfVAndNew(
'_idts',
trackingDatabaseHelper.getFirstVisitUnix().toString(),
);
// Device resolution
data.addIfVAndNew('res', '${size.width}x${size.height}');
data.addIfVAndNew('lang', Localizations.localeOf(context).languageCode);
data.addIfVAndNew('country', Localizations.localeOf(context).countryCode);

return _track(
_initAction,
data,
);
}

// TODO(m123): Matomo removes leading 0 from the barcode
static Future<bool> trackScannedProduct({required String barcode}) => _track(
_scanAction,
<String, String>{
_eventCategory: 'Scanner',
_eventAction: 'Scanned',
_eventValue: barcode,
},
);

static Future<bool> trackProductPageOpen({
required Product product,
}) {
final Map<String, String> data = <String, String>{
_eventCategory: 'Product page',
_eventAction: 'opened',
};
data.addIfVAndNew(_eventValue, product.productName);
data.addIfVAndNew(_eventName, product.productName);

return _track(
_productPageAction,
data,
);
}

static Future<bool> trackKnowledgePanelOpen({
String? knowledgePanelName,
}) {
final Map<String, String> data = <String, String>{
_eventCategory: 'Knowledge panel',
_eventAction: 'opened',
};
data.addIfVAndNew(_eventName, knowledgePanelName);

return _track(
_knowledgePanelAction,
data,
);
}

static Future<bool> trackPersonalizedRanking({
required String title,
required int products,
required int goodProducts,
required int badProducts,
required int unknownProducts,
}) =>
_track(
_personalizedRankingAction,
<String, String>{
'title': title,
'productsCount': '$products',
'goodProducts': '$goodProducts',
'badProducts': '$badProducts',
'unkownProducts': '$unknownProducts',
},
);

static void trackSearch({
required String search,
String? searchCategory,
int? searchCount,
}) {
final Map<String, String> data = <String, String>{
'search': search,
};
data.addIfVAndNew('search_cat', searchCategory);
data.addIfVAndNew('search_count', searchCount);

if (data.toString() == latestSearch) {
return;
}
latestSearch = data.toString();

_track(
_searchAction,
data,
);
}

static Future<bool> trackOpenLink({required String url}) => _track(
_linkAction,
<String, String>{
'url': url,
'link': url,
},
);

static Future<bool> _track(String actionName, Map<String, String> data) {
final DateTime date = DateTime.now();
final Map<String, String> addedData = <String, String>{
'action_name': actionName,
//Random number to avoid the tracking request being cached by the browser or a proxy.
'rand': Random().nextInt(1000).toString(),
//Adding the tracking time
'h': date.hour.toString(),
'm': date.minute.toString(),
's': date.second.toString(),
};
// User identifier
addedData.addIfVAndNew('uid', _getId());
addedData.addAll(data);

return MatomoForever.sendDataOrBulk(addedData);
}

static String? _getId() {
return kDebugMode ? 'smoothie-debug' : OpenFoodAPIConfiguration.uuid;
}
}
Loading

0 comments on commit 7783e41

Please sign in to comment.