📈 Tracking data's analytics service
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:island/services/analytics_service.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
@@ -256,8 +257,11 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
|
|
||||||
void setThemeMode(String value) {
|
void setThemeMode(String value) {
|
||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
|
final oldValue = state.themeMode;
|
||||||
prefs.setString(kAppThemeMode, value);
|
prefs.setString(kAppThemeMode, value);
|
||||||
state = state.copyWith(themeMode: value);
|
state = state.copyWith(themeMode: value);
|
||||||
|
|
||||||
|
AnalyticsService().logThemeChanged(oldValue ?? 'system', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setAppTransparentBackground(double value) {
|
void setAppTransparentBackground(double value) {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -13,6 +9,7 @@ import 'package:island/models/account.dart';
|
|||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:island/services/analytics_service.dart';
|
||||||
|
|
||||||
class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
|
class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
|
||||||
@override
|
@override
|
||||||
@@ -31,9 +28,7 @@ class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
|
|||||||
final response = await client.get('/pass/accounts/me');
|
final response = await client.get('/pass/accounts/me');
|
||||||
final user = SnAccount.fromJson(response.data);
|
final user = SnAccount.fromJson(response.data);
|
||||||
|
|
||||||
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
|
AnalyticsService().setUserId(user.id);
|
||||||
FirebaseAnalytics.instance.setUserId(id: user.id);
|
|
||||||
}
|
|
||||||
return user;
|
return user;
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
if (error is DioException) {
|
if (error is DioException) {
|
||||||
@@ -91,9 +86,8 @@ class UserInfoNotifier extends AsyncNotifier<SnAccount?> {
|
|||||||
final prefs = ref.read(sharedPreferencesProvider);
|
final prefs = ref.read(sharedPreferencesProvider);
|
||||||
await prefs.remove(kTokenPairStoreKey);
|
await prefs.remove(kTokenPairStoreKey);
|
||||||
ref.invalidate(tokenProvider);
|
ref.invalidate(tokenProvider);
|
||||||
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
|
AnalyticsService().setUserId(null);
|
||||||
FirebaseAnalytics.instance.setUserId(id: null);
|
AnalyticsService().logLogout();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import "package:island/pods/chat/chat_online_count.dart";
|
|||||||
import "package:island/pods/config.dart";
|
import "package:island/pods/config.dart";
|
||||||
import "package:island/pods/userinfo.dart";
|
import "package:island/pods/userinfo.dart";
|
||||||
import "package:island/screens/chat/search_messages.dart";
|
import "package:island/screens/chat/search_messages.dart";
|
||||||
|
import "package:island/services/analytics_service.dart";
|
||||||
import "package:island/services/file_uploader.dart";
|
import "package:island/services/file_uploader.dart";
|
||||||
import "package:island/services/responsive.dart";
|
import "package:island/services/responsive.dart";
|
||||||
import "package:island/widgets/alert.dart";
|
import "package:island/widgets/alert.dart";
|
||||||
@@ -55,6 +56,16 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final onlineCount = ref.watch(chatOnlineCountProvider(id));
|
final onlineCount = ref.watch(chatOnlineCountProvider(id));
|
||||||
final settings = ref.watch(appSettingsProvider);
|
final settings = ref.watch(appSettingsProvider);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (!chatRoom.isLoading && chatRoom.value != null) {
|
||||||
|
AnalyticsService().logChatRoomOpened(
|
||||||
|
id,
|
||||||
|
chatRoom.value!.isCommunity == true ? 'group' : 'direct',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
if (chatIdentity.isLoading || chatRoom.isLoading) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(leading: const PageBackButton()),
|
appBar: AppBar(leading: const PageBackButton()),
|
||||||
|
|||||||
519
lib/services/analytics_service.dart
Normal file
519
lib/services/analytics_service.dart
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:island/talker.dart';
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
static final AnalyticsService _instance = AnalyticsService._internal();
|
||||||
|
factory AnalyticsService() => _instance;
|
||||||
|
AnalyticsService._internal();
|
||||||
|
|
||||||
|
final _analytics = FirebaseAnalytics.instance;
|
||||||
|
bool _enabled = true;
|
||||||
|
|
||||||
|
bool get _supportsAnalytics =>
|
||||||
|
Platform.isAndroid || Platform.isIOS || Platform.isMacOS;
|
||||||
|
|
||||||
|
void logEvent(String name, Map<String, Object>? parameters) {
|
||||||
|
if (!_enabled || !_supportsAnalytics) return;
|
||||||
|
try {
|
||||||
|
_analytics.logEvent(name: name, parameters: parameters);
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[Analytics] Failed to log event $name: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEnabled(bool enabled) {
|
||||||
|
_enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUserId(String? id) {
|
||||||
|
if (!_supportsAnalytics) return;
|
||||||
|
try {
|
||||||
|
_analytics.setUserId(id: id);
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[Analytics] Failed to set user ID: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAppOpen() {
|
||||||
|
logEvent('app_open', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logLogin(String authMethod) {
|
||||||
|
logEvent('login', {'auth_method': authMethod, 'platform': _getPlatform()});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logLogout() {
|
||||||
|
logEvent('logout', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostViewed(String postId, String postType, String viewSource) {
|
||||||
|
logEvent('post_viewed', {
|
||||||
|
'post_id': postId,
|
||||||
|
'post_type': postType,
|
||||||
|
'view_source': viewSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostCreated(
|
||||||
|
String postType,
|
||||||
|
String visibility,
|
||||||
|
bool hasAttachments,
|
||||||
|
String publisherId,
|
||||||
|
) {
|
||||||
|
logEvent('post_created', {
|
||||||
|
'post_type': postType,
|
||||||
|
'visibility': visibility,
|
||||||
|
'has_attachments': hasAttachments,
|
||||||
|
'publisher_id': publisherId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostReacted(
|
||||||
|
String postId,
|
||||||
|
String reactionSymbol,
|
||||||
|
int attitude,
|
||||||
|
bool isRemoving,
|
||||||
|
) {
|
||||||
|
logEvent('post_reacted', {
|
||||||
|
'post_id': postId,
|
||||||
|
'reaction_symbol': reactionSymbol,
|
||||||
|
'attitude': attitude,
|
||||||
|
'is_removing': isRemoving,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostReplied(
|
||||||
|
String postId,
|
||||||
|
String parentId,
|
||||||
|
int characterCount,
|
||||||
|
bool hasAttachments,
|
||||||
|
) {
|
||||||
|
logEvent('post_replied', {
|
||||||
|
'post_id': postId,
|
||||||
|
'parent_id': parentId,
|
||||||
|
'character_count': characterCount,
|
||||||
|
'has_attachments': hasAttachments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostShared(String postId, String shareMethod, String postType) {
|
||||||
|
logEvent('post_shared', {
|
||||||
|
'post_id': postId,
|
||||||
|
'share_method': shareMethod,
|
||||||
|
'post_type': postType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostEdited(String postId, int contentChangeDelta) {
|
||||||
|
logEvent('post_edited', {
|
||||||
|
'post_id': postId,
|
||||||
|
'content_change_delta': contentChangeDelta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostDeleted(String postId, String postType, int timeSinceCreation) {
|
||||||
|
logEvent('post_deleted', {
|
||||||
|
'post_id': postId,
|
||||||
|
'post_type': postType,
|
||||||
|
'time_since_creation': timeSinceCreation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostPinned(String postId, String pinMode, String realmId) {
|
||||||
|
logEvent('post_pinned', {
|
||||||
|
'post_id': postId,
|
||||||
|
'pin_mode': pinMode,
|
||||||
|
'realm_id': realmId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostAwarded(
|
||||||
|
String postId,
|
||||||
|
double amount,
|
||||||
|
String attitude,
|
||||||
|
bool hasMessage,
|
||||||
|
) {
|
||||||
|
logEvent('post_awarded', {
|
||||||
|
'post_id': postId,
|
||||||
|
'amount': amount,
|
||||||
|
'attitude': attitude,
|
||||||
|
'has_message': hasMessage,
|
||||||
|
'currency': 'NSP',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostTranslated(
|
||||||
|
String postId,
|
||||||
|
String sourceLanguage,
|
||||||
|
String targetLanguage,
|
||||||
|
) {
|
||||||
|
logEvent('post_translated', {
|
||||||
|
'post_id': postId,
|
||||||
|
'source_language': sourceLanguage,
|
||||||
|
'target_language': targetLanguage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logPostForwarded(
|
||||||
|
String postId,
|
||||||
|
String originalPostId,
|
||||||
|
String publisherId,
|
||||||
|
) {
|
||||||
|
logEvent('post_forwarded', {
|
||||||
|
'post_id': postId,
|
||||||
|
'original_post_id': originalPostId,
|
||||||
|
'publisher_id': publisherId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logMessageSent(
|
||||||
|
String channelId,
|
||||||
|
String messageType,
|
||||||
|
bool hasAttachments,
|
||||||
|
int attachmentCount,
|
||||||
|
) {
|
||||||
|
logEvent('message_sent', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'message_type': messageType,
|
||||||
|
'has_attachments': hasAttachments,
|
||||||
|
'attachment_count': attachmentCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logMessageReceived(
|
||||||
|
String channelId,
|
||||||
|
String messageType,
|
||||||
|
bool isMentioned,
|
||||||
|
) {
|
||||||
|
logEvent('message_received', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'message_type': messageType,
|
||||||
|
'is_mentioned': isMentioned,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logMessageReplied(
|
||||||
|
String channelId,
|
||||||
|
String originalMessageId,
|
||||||
|
int replyDepth,
|
||||||
|
) {
|
||||||
|
logEvent('message_replied', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'original_message_id': originalMessageId,
|
||||||
|
'reply_depth': replyDepth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logMessageEdited(
|
||||||
|
String channelId,
|
||||||
|
String messageId,
|
||||||
|
int contentChangeDelta,
|
||||||
|
) {
|
||||||
|
logEvent('message_edited', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'message_id': messageId,
|
||||||
|
'content_change_delta': contentChangeDelta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logMessageDeleted(
|
||||||
|
String channelId,
|
||||||
|
String messageId,
|
||||||
|
String messageType,
|
||||||
|
bool isOwn,
|
||||||
|
) {
|
||||||
|
logEvent('message_deleted', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'message_id': messageId,
|
||||||
|
'message_type': messageType,
|
||||||
|
'is_own': isOwn,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logChatRoomOpened(String channelId, String roomType) {
|
||||||
|
logEvent('chat_room_opened', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'room_type': roomType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logChatJoined(String channelId, String roomType, bool isPublic) {
|
||||||
|
logEvent('chat_joined', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'room_type': roomType,
|
||||||
|
'is_public': isPublic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logChatLeft(String channelId, String roomType) {
|
||||||
|
logEvent('chat_left', {'channel_id': channelId, 'room_type': roomType});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logChatInvited(String channelId, String invitedUserId) {
|
||||||
|
logEvent('chat_invited', {
|
||||||
|
'channel_id': channelId,
|
||||||
|
'invited_user_id': invitedUserId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFileUploaded(
|
||||||
|
String fileType,
|
||||||
|
String fileSizeCategory,
|
||||||
|
String uploadSource,
|
||||||
|
) {
|
||||||
|
logEvent('file_uploaded', {
|
||||||
|
'file_type': fileType,
|
||||||
|
'file_size_category': fileSizeCategory,
|
||||||
|
'upload_source': uploadSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFileDownloaded(
|
||||||
|
String fileType,
|
||||||
|
String fileSizeCategory,
|
||||||
|
String downloadMethod,
|
||||||
|
) {
|
||||||
|
logEvent('file_downloaded', {
|
||||||
|
'file_type': fileType,
|
||||||
|
'file_size_category': fileSizeCategory,
|
||||||
|
'download_method': downloadMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFileDeleted(
|
||||||
|
String fileType,
|
||||||
|
String fileSizeCategory,
|
||||||
|
String deleteSource,
|
||||||
|
bool isBatchDelete,
|
||||||
|
int batchCount,
|
||||||
|
) {
|
||||||
|
logEvent('file_deleted', {
|
||||||
|
'file_type': fileType,
|
||||||
|
'file_size_category': fileSizeCategory,
|
||||||
|
'delete_source': deleteSource,
|
||||||
|
'is_batch_delete': isBatchDelete,
|
||||||
|
'batch_count': batchCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logStickerUsed(String packId, String stickerSlug, String context) {
|
||||||
|
logEvent('sticker_used', {
|
||||||
|
'pack_id': packId,
|
||||||
|
'sticker_slug': stickerSlug,
|
||||||
|
'context': context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logStickerPackAdded(String packId, String packName, int stickerCount) {
|
||||||
|
logEvent('sticker_pack_added', {
|
||||||
|
'pack_id': packId,
|
||||||
|
'pack_name': packName,
|
||||||
|
'sticker_count': stickerCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logStickerPackViewed(String packId, int stickerCount, bool isOwned) {
|
||||||
|
logEvent('sticker_pack_viewed', {
|
||||||
|
'pack_id': packId,
|
||||||
|
'sticker_count': stickerCount,
|
||||||
|
'is_owned': isOwned,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logSearchPerformed(
|
||||||
|
String searchType,
|
||||||
|
String query,
|
||||||
|
int resultCount,
|
||||||
|
bool hasFilters,
|
||||||
|
) {
|
||||||
|
logEvent('search_performed', {
|
||||||
|
'search_type': searchType,
|
||||||
|
'query': query,
|
||||||
|
'result_count': resultCount,
|
||||||
|
'has_filters': hasFilters,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFeedSubscribed(String feedId, String feedUrl, int articleCount) {
|
||||||
|
logEvent('feed_subscribed', {
|
||||||
|
'feed_id': feedId,
|
||||||
|
'feed_url': feedUrl,
|
||||||
|
'article_count': articleCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFeedUnsubscribed(String feedId, String feedUrl) {
|
||||||
|
logEvent('feed_unsubscribed', {'feed_id': feedId, 'feed_url': feedUrl});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logWalletTransfer(
|
||||||
|
double amount,
|
||||||
|
String currency,
|
||||||
|
String payeeId,
|
||||||
|
bool hasRemark,
|
||||||
|
) {
|
||||||
|
logEvent('wallet_transfer', {
|
||||||
|
'amount': amount,
|
||||||
|
'currency': currency,
|
||||||
|
'payee_id': payeeId,
|
||||||
|
'has_remark': hasRemark,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logWalletBalanceChecked(List<String> currenciesViewed) {
|
||||||
|
logEvent('wallet_balance_checked', {
|
||||||
|
'currencies_viewed': currenciesViewed.join(','),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logWalletOpened(String activeTab) {
|
||||||
|
logEvent('wallet_opened', {'active_tab': activeTab});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logRealmJoined(String realmSlug, String realmType) {
|
||||||
|
logEvent('realm_joined', {
|
||||||
|
'realm_slug': realmSlug,
|
||||||
|
'realm_type': realmType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logRealmLeft(String realmSlug) {
|
||||||
|
logEvent('realm_left', {'realm_slug': realmSlug});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFriendAdded(String friendId, String pickerMethod) {
|
||||||
|
logEvent('friend_added', {
|
||||||
|
'friend_id': friendId,
|
||||||
|
'picker_method': pickerMethod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFriendRemoved(String relationshipId, String relationshipType) {
|
||||||
|
logEvent('friend_removed', {
|
||||||
|
'relationship_id': relationshipId,
|
||||||
|
'relationship_type': relationshipType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logUserBlocked(String blockedUserId, String previousRelationship) {
|
||||||
|
logEvent('user_blocked', {
|
||||||
|
'blocked_user_id': blockedUserId,
|
||||||
|
'previous_relationship': previousRelationship,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logUserUnblocked(String unblockedUserId) {
|
||||||
|
logEvent('user_unblocked', {'unblocked_user_id': unblockedUserId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFriendRequestAccepted(String requesterId) {
|
||||||
|
logEvent('friend_request_accepted', {'requester_id': requesterId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logFriendRequestDeclined(String requesterId) {
|
||||||
|
logEvent('friend_request_declined', {'requester_id': requesterId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logThemeChanged(String oldMode, String newMode) {
|
||||||
|
logEvent('theme_changed', {'old_mode': oldMode, 'new_mode': newMode});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logLanguageChanged(String oldLanguage, String newLanguage) {
|
||||||
|
logEvent('language_changed', {
|
||||||
|
'old_language': oldLanguage,
|
||||||
|
'new_language': newLanguage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAiQuerySent(
|
||||||
|
int messageLength,
|
||||||
|
String contextType,
|
||||||
|
int attachedPostsCount,
|
||||||
|
) {
|
||||||
|
logEvent('ai_query_sent', {
|
||||||
|
'message_length': messageLength,
|
||||||
|
'context_type': contextType,
|
||||||
|
'attached_posts_count': attachedPostsCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAiResponseReceived(int responseThoughtCount, String sequenceId) {
|
||||||
|
logEvent('ai_response_received', {
|
||||||
|
'response_thought_count': responseThoughtCount,
|
||||||
|
'sequence_id': sequenceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logShuffleViewed(int postIndex, int totalPostsLoaded) {
|
||||||
|
logEvent('shuffle_viewed', {
|
||||||
|
'post_index': postIndex,
|
||||||
|
'total_posts_loaded': totalPostsLoaded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logDraftSaved(String draftId, String postType, bool hasContent) {
|
||||||
|
logEvent('draft_saved', {
|
||||||
|
'draft_id': draftId,
|
||||||
|
'post_type': postType,
|
||||||
|
'has_content': hasContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logDraftDeleted(String draftId, String postType) {
|
||||||
|
logEvent('draft_deleted', {'draft_id': draftId, 'post_type': postType});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logCategoryViewed(String categorySlug, String categoryId) {
|
||||||
|
logEvent('category_viewed', {
|
||||||
|
'category_slug': categorySlug,
|
||||||
|
'category_id': categoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logTagViewed(String tagSlug, String tagId) {
|
||||||
|
logEvent('tag_viewed', {'tag_slug': tagSlug, 'tag_id': tagId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logCategorySubscribed(String categorySlug, String categoryId) {
|
||||||
|
logEvent('category_subscribed', {
|
||||||
|
'category_slug': categorySlug,
|
||||||
|
'category_id': categoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logTagSubscribed(String tagSlug, String tagId) {
|
||||||
|
logEvent('tag_subscribed', {'tag_slug': tagSlug, 'tag_id': tagId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logNotificationViewed() {
|
||||||
|
logEvent('notification_viewed', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logNotificationActioned(String actionType, String notificationType) {
|
||||||
|
logEvent('notification_actioned', {
|
||||||
|
'action_type': actionType,
|
||||||
|
'notification_type': notificationType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logProfileUpdated(List<String> fieldsUpdated) {
|
||||||
|
logEvent('profile_updated', {'fields_updated': fieldsUpdated.join(',')});
|
||||||
|
}
|
||||||
|
|
||||||
|
void logAvatarChanged(String imageSource, bool isCropped) {
|
||||||
|
logEvent('avatar_changed', {
|
||||||
|
'image_source': imageSource,
|
||||||
|
'is_cropped': isCropped,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getPlatform() {
|
||||||
|
if (Platform.isAndroid) return 'android';
|
||||||
|
if (Platform.isIOS) return 'ios';
|
||||||
|
if (Platform.isMacOS) return 'macos';
|
||||||
|
if (Platform.isWindows) return 'windows';
|
||||||
|
if (Platform.isLinux) return 'linux';
|
||||||
|
return 'web';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
import 'package:flutter_app_intents/flutter_app_intents.dart';
|
import 'package:flutter_app_intents/flutter_app_intents.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:island/models/auth.dart';
|
import 'package:island/models/auth.dart';
|
||||||
@@ -532,6 +533,17 @@ class AppIntentsService {
|
|||||||
return '${DateTime.now().millisecondsSinceEpoch}-${DateTime.now().microsecondsSinceEpoch}';
|
return '${DateTime.now().millisecondsSinceEpoch}-${DateTime.now().microsecondsSinceEpoch}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _logDonation(String eventName, Map<String, Object> parameters) {
|
||||||
|
try {
|
||||||
|
FirebaseAnalytics.instance.logEvent(
|
||||||
|
name: eventName,
|
||||||
|
parameters: parameters.isEmpty ? null : parameters,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
talker.warning('[AppIntents] Failed to log analytics: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<AppIntentResult> _handleCheckUnreadChatsIntent(
|
Future<AppIntentResult> _handleCheckUnreadChatsIntent(
|
||||||
Map<String, dynamic> parameters,
|
Map<String, dynamic> parameters,
|
||||||
) async {
|
) async {
|
||||||
@@ -616,6 +628,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.8,
|
relevanceScore: 0.8,
|
||||||
context: {'feature': 'chat', 'userAction': true},
|
context: {'feature': 'chat', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('open_chat', {'channel_id': channelId});
|
||||||
talker.info('[AppIntents] Donated open_chat intent');
|
talker.info('[AppIntents] Donated open_chat intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate open_chat', e, stack);
|
talker.error('[AppIntents] Failed to donate open_chat', e, stack);
|
||||||
@@ -631,6 +644,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.8,
|
relevanceScore: 0.8,
|
||||||
context: {'feature': 'posts', 'userAction': true},
|
context: {'feature': 'posts', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('open_post', {'post_id': postId});
|
||||||
talker.info('[AppIntents] Donated open_post intent');
|
talker.info('[AppIntents] Donated open_post intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate open_post', e, stack);
|
talker.error('[AppIntents] Failed to donate open_post', e, stack);
|
||||||
@@ -646,6 +660,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.9,
|
relevanceScore: 0.9,
|
||||||
context: {'feature': 'compose', 'userAction': true},
|
context: {'feature': 'compose', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('open_compose', {});
|
||||||
talker.info('[AppIntents] Donated compose intent');
|
talker.info('[AppIntents] Donated compose intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate compose', e, stack);
|
talker.error('[AppIntents] Failed to donate compose', e, stack);
|
||||||
@@ -661,6 +676,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.7,
|
relevanceScore: 0.7,
|
||||||
context: {'feature': 'search', 'userAction': true},
|
context: {'feature': 'search', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('search_content', {'query': query});
|
||||||
talker.info('[AppIntents] Donated search intent');
|
talker.info('[AppIntents] Donated search intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate search', e, stack);
|
talker.error('[AppIntents] Failed to donate search', e, stack);
|
||||||
@@ -676,6 +692,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.6,
|
relevanceScore: 0.6,
|
||||||
context: {'feature': 'notifications', 'userAction': true},
|
context: {'feature': 'notifications', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('check_notifications', {});
|
||||||
talker.info('[AppIntents] Donated check_notifications intent');
|
talker.info('[AppIntents] Donated check_notifications intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error(
|
talker.error(
|
||||||
@@ -695,6 +712,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.8,
|
relevanceScore: 0.8,
|
||||||
context: {'feature': 'chat', 'userAction': true},
|
context: {'feature': 'chat', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('send_message', {'channel_id': channelId});
|
||||||
talker.info('[AppIntents] Donated send_message intent');
|
talker.info('[AppIntents] Donated send_message intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate send_message', e, stack);
|
talker.error('[AppIntents] Failed to donate send_message', e, stack);
|
||||||
@@ -710,6 +728,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.7,
|
relevanceScore: 0.7,
|
||||||
context: {'feature': 'chat', 'userAction': true},
|
context: {'feature': 'chat', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('read_messages', {'channel_id': channelId});
|
||||||
talker.info('[AppIntents] Donated read_messages intent');
|
talker.info('[AppIntents] Donated read_messages intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error('[AppIntents] Failed to donate read_messages', e, stack);
|
talker.error('[AppIntents] Failed to donate read_messages', e, stack);
|
||||||
@@ -725,6 +744,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.7,
|
relevanceScore: 0.7,
|
||||||
context: {'feature': 'chat', 'userAction': true},
|
context: {'feature': 'chat', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('check_unread_chats', {});
|
||||||
talker.info('[AppIntents] Donated check_unread_chats intent');
|
talker.info('[AppIntents] Donated check_unread_chats intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error(
|
talker.error(
|
||||||
@@ -744,6 +764,7 @@ class AppIntentsService {
|
|||||||
relevanceScore: 0.6,
|
relevanceScore: 0.6,
|
||||||
context: {'feature': 'notifications', 'userAction': true},
|
context: {'feature': 'notifications', 'userAction': true},
|
||||||
);
|
);
|
||||||
|
_logDonation('mark_notifications_read', {});
|
||||||
talker.info('[AppIntents] Donated mark_notifications_read intent');
|
talker.info('[AppIntents] Donated mark_notifications_read intent');
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
talker.error(
|
talker.error(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:island/widgets/post/post_shared.dart';
|
|||||||
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
|
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
|
||||||
import 'package:screenshot/screenshot.dart';
|
import 'package:screenshot/screenshot.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import 'package:island/services/analytics_service.dart';
|
||||||
|
|
||||||
/// Shares a post as a screenshot image
|
/// Shares a post as a screenshot image
|
||||||
Future<void> sharePostAsScreenshot(
|
Future<void> sharePostAsScreenshot(
|
||||||
@@ -62,5 +63,9 @@ Future<void> sharePostAsScreenshot(
|
|||||||
.catchError((err) {
|
.catchError((err) {
|
||||||
if (context.mounted) hideLoadingModal(context);
|
if (context.mounted) hideLoadingModal(context);
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
|
})
|
||||||
|
.whenComplete(() {
|
||||||
|
final postTypeStr = post.type == 0 ? 'regular' : 'article';
|
||||||
|
AnalyticsService().logPostShared(post.id, 'screenshot', postTypeStr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import 'package:island/widgets/post/compose_recorder.dart';
|
|||||||
import 'package:island/pods/drive/file_pool.dart';
|
import 'package:island/pods/drive/file_pool.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
|
import 'package:island/services/analytics_service.dart';
|
||||||
|
|
||||||
class ComposeState {
|
class ComposeState {
|
||||||
final TextEditingController titleController;
|
final TextEditingController titleController;
|
||||||
@@ -738,6 +739,17 @@ class ComposeLogic {
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
eventBus.fire(PostCreatedEvent());
|
eventBus.fire(PostCreatedEvent());
|
||||||
|
|
||||||
|
final postTypeStr = state.postType == 0 ? 'regular' : 'article';
|
||||||
|
final visibilityStr = state.visibility.value.toString();
|
||||||
|
final publisherId = state.currentPublisher.value?.id ?? 'unknown';
|
||||||
|
|
||||||
|
AnalyticsService().logPostCreated(
|
||||||
|
postTypeStr,
|
||||||
|
visibilityStr,
|
||||||
|
state.attachments.value.isNotEmpty,
|
||||||
|
publisherId,
|
||||||
|
);
|
||||||
|
|
||||||
return post;
|
return post;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showErrorAlert(err);
|
showErrorAlert(err);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import 'package:island/widgets/post/compose_sheet.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:super_context_menu/super_context_menu.dart';
|
import 'package:super_context_menu/super_context_menu.dart';
|
||||||
|
import 'package:island/services/analytics_service.dart';
|
||||||
|
|
||||||
const kAvailableStickers = {
|
const kAvailableStickers = {
|
||||||
'angry',
|
'angry',
|
||||||
@@ -367,7 +368,15 @@ class PostItem extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
|
|
||||||
|
AnalyticsService().logPostReacted(
|
||||||
|
item.id,
|
||||||
|
symbol,
|
||||||
|
attitude,
|
||||||
|
isRemoving,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
reacting.value = false;
|
reacting.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user