Skip to content

Commit

Permalink
Merge branch 'develop' into feature/openfoodfacts#944
Browse files Browse the repository at this point in the history
  • Loading branch information
monsieurtanuki authored Feb 18, 2022
2 parents d31b7ff + 0c9552e commit 94b3c3c
Show file tree
Hide file tree
Showing 23 changed files with 948 additions and 356 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dartdoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
run: pub global activate dartdoc && dartdoc

- name: Deploy API documentation to Github Pages
uses: JamesIves/[email protected].3
uses: JamesIves/[email protected].5
with:
BRANCH: gh-pages
FOLDER: packages/smooth_app/doc/api/
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 All @@ -32,7 +33,6 @@ class ContinuousScanModel with ChangeNotifier {
late DaoProduct _daoProduct;
late DaoProductList _daoProductList;

bool get hasMoreThanOneProduct => getBarcodes().length > 1;
ProductList get productList => _productList;

List<String> getBarcodes() => _barcodes;
Expand Down Expand Up @@ -99,6 +99,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 94b3c3c

Please sign in to comment.