From 4dd6e21823fe3f819593488328717297ef60688f Mon Sep 17 00:00:00 2001 From: Tyler Jeong Date: Fri, 15 Sep 2023 10:09:12 +0900 Subject: [PATCH] Add 4.0.12. --- CHANGELOG.md | 91 +++---- README.md | 2 +- lib/src/internal/main/chat/chat.dart | 5 +- .../main/chat_manager/command_manager.dart | 1 - .../main/chat_manager/connection_manager.dart | 37 ++- .../internal/main/stats/api_result_stat.dart | 85 +++++++ .../main/stats/default_stat_prefs.dart | 4 + .../main/stats/notification_stat.dart | 15 +- lib/src/internal/main/stats/stat_manager.dart | 223 +++++++++++++----- .../internal/main/stats/ws_connect_stat.dart | 76 ++++++ lib/src/internal/network/http/api_client.dart | 67 ++++-- .../network/http/http_client/http_client.dart | 140 +++++------ .../request/main/upload_stat_request.dart | 4 +- lib/src/public/core/message/base_message.dart | 15 +- pubspec.yaml | 2 +- 15 files changed, 539 insertions(+), 228 deletions(-) create mode 100644 lib/src/internal/main/stats/api_result_stat.dart create mode 100644 lib/src/internal/main/stats/ws_connect_stat.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 85f42b97..069478fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,61 +1,67 @@ +## v4.0.12 (Sep 15, 2023) + +### Improvements +- Fixed the bug regarding parent FileMessage type +- Updated regarding statistics + ## v4.0.11 (Sep 12, 2023) ### Improvements -- Fixed the bug regarding the url encoding of `userId`. +- Fixed the bug regarding the url encoding of `userId` ## v4.0.10 (Aug 31, 2023) ### Improvements -- Fixed the bug regarding the `FeedChannel` caching. +- Fixed the bug regarding the `FeedChannel` caching ## v4.0.9 (Aug 30, 2023) ### Improvements -- Fixed the bug regarding the `hasNext` in `BaseMessageCollection`. -- Fixed the bug when the `reverse` is `true` in `MessageListParams` regarding `BaseMessageCollection`. -- Improved stability. +- Fixed the bug regarding the `hasNext` in `BaseMessageCollection` +- Fixed the bug when the `reverse` is `true` in `MessageListParams` regarding `BaseMessageCollection` +- Improved stability ## v4.0.8 (Aug 22, 2023) ### Features -- Replaced `Map templateVariables` with `Map templateVariables` in `NotificationData`. -- Added `tags` in `NotificationData`. +- Replaced `Map templateVariables` with `Map templateVariables` in `NotificationData` +- Added `tags` in `NotificationData` ### Improvements -- Improved stability. +- Improved stability ## v4.0.7 (Aug 18, 2023) ### Features -- Added `notificationData` in `BaseMessage`. +- Added `notificationData` in `BaseMessage` ### Improvements -- Fixed the bug regarding `unreadMessageCount` in `FeedChannel`. +- Fixed the bug regarding `unreadMessageCount` in `FeedChannel` ## v4.0.6 (Aug 16, 2023) ### Features #### Notification -- Added `isTemplateLabelEnabled`, `isCategoryFilterEnabled` and `notificationCategories` in `FeedChannel`. -- Added `authenticateFeed()`, `refreshNotificationCollections()` in `SendbirdChat`. +- Added `isTemplateLabelEnabled`, `isCategoryFilterEnabled` and `notificationCategories` in `FeedChannel` +- Added `authenticateFeed()`, `refreshNotificationCollections()` in `SendbirdChat` ### Improvements -- Improved stability. +- Improved stability ## v4.0.5 (Jul 14, 2023) ### Features -- Added `SendbirdStatistics` for internal use. +- Added `SendbirdStatistics` for internal use ### Improvements -- Improved stability. +- Improved stability ## v4.0.4 (Jul 3, 2023) ### Improvements -- Fixed the bug regarding `resendFileMessage()`. -- Fixed the bug regarding connectivity events. +- Fixed the bug regarding `resendFileMessage()` +- Fixed the bug regarding connectivity events ## v4.0.3 (Jun 30, 2023) @@ -63,51 +69,48 @@ #### FeedChannel - Added `FeedChannelListQuery` -- Added `FeedChannel`. -- Added `feed` in `ChannelType`. -- Added `getMyFeedChannelChangeLogs()` with `FeedChannelChangeLogsParams` in SendbirdChat. +- Added `FeedChannel` +- Added `feed` in `ChannelType` +- Added `getMyFeedChannelChangeLogs()` with `FeedChannelChangeLogsParams` in SendbirdChat - Added `getTotalUnreadMessageCountWithFeedChannel() - ` in SendbirdChat. -- Added `FeedChannelHandler`. -- Added `onTotalUnreadMessageCountChanged()` in `UserEventHandler` and `UnreadMessageCount`. + ` in SendbirdChat +- Added `FeedChannelHandler` +- Added `onTotalUnreadMessageCountChanged()` in `UserEventHandler` and `UnreadMessageCount` #### Collection for notifications -- Added `NotificationCollection`, `NotificationCollectionHandler` and `NotificationContext`. -- Added `BaseMessageCollection`, `BaseMessageCollectionHandler` and `BaseMessageContext`. -- Added `FeedChannelContext`, `BaseChannelContext`. +- Added `NotificationCollection`, `NotificationCollectionHandler` and `NotificationContext` +- Added `BaseMessageCollection`, `BaseMessageCollectionHandler` and `BaseMessageContext` +- Added `FeedChannelContext`, `BaseChannelContext` #### ChatNotification for GroupChannel -- Added `isChatNotification` in GroupChannel. -- Added `includeChatNotification` in `GroupChannelListQuery` and `GroupChannelChangeLogsParams`. +- Added `isChatNotification` in GroupChannel +- Added `includeChatNotification` in `GroupChannelListQuery` and `GroupChannelChangeLogsParams` #### Setting and Template for Notification -- Added `getGlobalNotificationChannelSetting() - ` and `GlobalNotificationChannelSetting` in SendbirdChat. -- Added `getNotificationTemplateListByToken() - ` with `NotificationTemplateListParams` and `NotificationTemplateList` in SendbirdChat. -- Added `getNotificationTemplate() - ` and `NotificationTemplate` in SendbirdChat. +- Added `getGlobalNotificationChannelSetting()` and `GlobalNotificationChannelSetting` in SendbirdChat +- Added `getNotificationTemplateListByToken()` with `NotificationTemplateListParams` and `NotificationTemplateList` in SendbirdChat +- Added `getNotificationTemplate()` and `NotificationTemplate` in SendbirdChat #### NotificationInfo -- Added `NotificationInfo`. -- Added `notificationInfo` in `AppInfo`. +- Added `NotificationInfo` +- Added `notificationInfo` in `AppInfo` ### Improvements -- Improved stability. +- Improved stability ## v4.0.2 (Jun 23, 2023) -- Improved stability. +- Improved stability ## v4.0.1 (Jun 14, 2023) -- Improved stability. +- Improved stability ## v4.0.0 (May 31, 2023) -> To see detailed changes, please refer to the [migration guide](https://sendbird.com/docs/chat/v4/flutter/getting-started/migration-guide). +> To see detailed changes, please refer to the [migration guide](https://sendbird.com/docs/chat/v4/flutter/getting-started/migration-guide) ### Features -- Added `GroupChannelCollection`, `GroupChannelContext` and `GroupChannelCollectionHandler`. -- Added `MessageCollection`, `MessageContext` and `MessageCollectionHandler`. -- Added `enum CollectionEventSource`. +- Added `GroupChannelCollection`, `GroupChannelContext` and `GroupChannelCollectionHandler` +- Added `MessageCollection`, `MessageContext` and `MessageCollectionHandler` +- Added `enum CollectionEventSource` ## v3 Changelog -Please refer to [this page](https://github.com/sendbird/sendbird-chat-sdk-flutter/blob/v3/CHANGELOG.md). +Please refer to [this page](https://github.com/sendbird/sendbird-chat-sdk-flutter/blob/v3/CHANGELOG.md) diff --git a/README.md b/README.md index e097b671..65bb74b5 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Before installing Sendbird Chat SDK, you need to create a Sendbird application o ```yaml dependencies: - sendbird_chat_sdk: ^4.0.11 + sendbird_chat_sdk: ^4.0.12 ``` - Run `flutter pub get` command in your project directory. diff --git a/lib/src/internal/main/chat/chat.dart b/lib/src/internal/main/chat/chat.dart index f6fe7386..9524bc5a 100644 --- a/lib/src/internal/main/chat/chat.dart +++ b/lib/src/internal/main/chat/chat.dart @@ -58,7 +58,7 @@ part 'chat_notifications.dart'; part 'chat_push.dart'; part 'chat_user.dart'; -const sdkVersion = '4.0.11'; +const sdkVersion = '4.0.12'; // Internal implementation for main class. Do not directly access this class. class Chat with WidgetsBindingObserver { @@ -117,13 +117,14 @@ class Chat with WidgetsBindingObserver { sessionManager = SessionManager(chat: this); commandManager = CommandManager(chat: this); eventManager = EventManager(sessionManager: sessionManager); + statManager = StatManager(chat: this); apiClient = ApiClient( chatContext: chatContext, sessionManager: sessionManager, + statManager: statManager, ); // HttpClient eventDispatcher = EventDispatcher(chat: this); collectionManager = CollectionManager(chat: this); - statManager = StatManager(chat: this); _listenConnectivityChangedEvent(); } diff --git a/lib/src/internal/main/chat_manager/command_manager.dart b/lib/src/internal/main/chat_manager/command_manager.dart index 4d02fb77..70a5da4f 100644 --- a/lib/src/internal/main/chat_manager/command_manager.dart +++ b/lib/src/internal/main/chat_manager/command_manager.dart @@ -672,7 +672,6 @@ class CommandManager { } } -// System Future _processSystemEvent(Command cmd) async { final event = ChannelEvent.fromJsonWithChat(_chat, cmd.payload); diff --git a/lib/src/internal/main/chat_manager/connection_manager.dart b/lib/src/internal/main/chat_manager/connection_manager.dart index 061cc821..e708121f 100644 --- a/lib/src/internal/main/chat_manager/connection_manager.dart +++ b/lib/src/internal/main/chat_manager/connection_manager.dart @@ -158,17 +158,35 @@ class ConnectionManager { runZonedGuarded(() { sbLog.d(StackTrace.current, 'webSocketClient?.connect()'); + + chat.statManager.startWsConnectStat(hostUrl: url); webSocketClient.connect(url); }, (e, s) async { sbLog.e(StackTrace.current, 'e: $e'); + changeState(DisconnectedState(chat: chat)); if (chat.chatContext.loginCompleter != null && !chat.chatContext.loginCompleter!.isCompleted) { if (e is SendbirdException) { + chat.statManager.endWsConnectStat( + hostUrl: url, + success: false, + errorCode: e.code, + errorDescription: e.message, + ); + chat.chatContext.loginCompleter?.completeError(e); } else { - chat.chatContext.loginCompleter - ?.completeError(WebSocketFailedException(message: e.toString())); + final exception = WebSocketFailedException(message: e.toString()); + + chat.statManager.endWsConnectStat( + hostUrl: url, + success: false, + errorCode: exception.code, + errorDescription: exception.message, + ); + + chat.chatContext.loginCompleter?.completeError(exception); } } }); @@ -176,11 +194,24 @@ class ConnectionManager { final user = await chat.chatContext.loginCompleter!.future .timeout(Duration(seconds: chat.chatContext.options.connectionTimeout), onTimeout: () async { + final e = LoginTimeoutException(); + + chat.statManager.endWsConnectStat( + hostUrl: url, + success: false, + errorCode: e.code, + errorDescription: e.name, + ); + await doDisconnect(logout: true); - throw LoginTimeoutException(); + throw e; }); // After 'LOGI' received + chat.statManager.endWsConnectStat( + hostUrl: url, + success: true, + ); return user; } diff --git a/lib/src/internal/main/stats/api_result_stat.dart b/lib/src/internal/main/stats/api_result_stat.dart new file mode 100644 index 00000000..583c6ffd --- /dev/null +++ b/lib/src/internal/main/stats/api_result_stat.dart @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Sendbird, Inc. All rights reserved. + +import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/default_stat.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_type.dart'; + +class ApiResultStat extends DefaultStat { + final String endpoint; // endpoint + final String method; // 'GET|POST|PUT|DELETE' + final bool success; // success or not + final int latency; // roundtrip latency + final int? errorCode; // error code if exist + final String? errorDescription; // detailed error message + + ApiResultStat({ + required int ts, + required this.endpoint, + required this.method, + required this.success, + required this.latency, + this.errorCode, + this.errorDescription, + }) : super(StatType.apiResult, ts); + + @override + Map toJson() { + final json = { + 'endpoint': endpoint, + 'method': method, + 'success': success, + 'latency': latency, + 'error_code': errorCode, + 'error_description': errorDescription, + }; + final result = super.toJson(); + result['data'] = json; + return result; + } + + // { + // 'stat_type' : 'api:result', + // 'ts': int, // timestamp for log creation, + // 'data' : { + // 'endpoint': String, + // 'method': String, + // 'success': bool, + // 'latency': int, + // 'error_code': int?, + // 'error_description': String?, + // }, + // } + static ApiResultStat? fromJson({ + required int ts, + required Map data, + }) { + try { + final String? endpoint = data['endpoint'] as String?; + final String? method = data['method'] as String?; + final bool? success = data['success'] as bool?; + final int? latency = data['latency'] as int?; + final int? errorCode = data['error_code'] as int?; + final String? errorDescription = data['error_description'] as String?; + + if (endpoint == null || + method == null || + success == null || + latency == null) { + return null; + } + + return ApiResultStat( + ts: ts, + endpoint: endpoint, + method: method, + success: success, + latency: latency, + errorCode: errorCode, + errorDescription: errorDescription, + ); + } catch (e) { + sbLog.d(StackTrace.current, 'e: ${e.toString()}'); + } + return null; + } +} diff --git a/lib/src/internal/main/stats/default_stat_prefs.dart b/lib/src/internal/main/stats/default_stat_prefs.dart index abc34911..8982ba65 100644 --- a/lib/src/internal/main/stats/default_stat_prefs.dart +++ b/lib/src/internal/main/stats/default_stat_prefs.dart @@ -4,10 +4,12 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/api_result_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/default_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/notification_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_type.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_utils.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/ws_connect_stat.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; @@ -79,7 +81,9 @@ class DefaultStatPrefs { if (statType != null) { switch (statType) { case StatType.apiResult: + return ApiResultStat.fromJson(ts: ts, data: data); case StatType.wsConnect: + return WsConnectStat.fromJson(ts: ts, data: data); case StatType.featureLocalCache: return null; case StatType.notificationStats: diff --git a/lib/src/internal/main/stats/notification_stat.dart b/lib/src/internal/main/stats/notification_stat.dart index e426440f..24022dc2 100644 --- a/lib/src/internal/main/stats/notification_stat.dart +++ b/lib/src/internal/main/stats/notification_stat.dart @@ -40,12 +40,23 @@ class NotificationStat extends DefaultStat { return result; } + // { + // 'stat_type' : 'noti:stats', + // 'ts': int, // timestamp for log creation, + // 'data' : { + // 'action': String, // 'clicked' + // 'template_key': String, + // 'channel_url': String, + // 'tags': List, + // 'message_id': int, + // 'source': 'notification', + // 'message_ts': int, + // }, + // } static NotificationStat? fromJson({ required int ts, required Map data, }) { - sbLog.d(StackTrace.current); - try { final String? action = data['action'] as String?; final String? templateKey = data['template_key'] as String?; diff --git a/lib/src/internal/main/stats/stat_manager.dart b/lib/src/internal/main/stats/stat_manager.dart index 18b87bf0..c4b9fd41 100644 --- a/lib/src/internal/main/stats/stat_manager.dart +++ b/lib/src/internal/main/stats/stat_manager.dart @@ -1,9 +1,9 @@ // Copyright (c) 2023 Sendbird, Inc. All rights reserved. -import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat/chat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/api_result_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/base_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/default_stat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/default_stat_prefs.dart'; @@ -11,6 +11,8 @@ import 'package:sendbird_chat_sdk/src/internal/main/stats/notification_stat.dart import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_state.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_type.dart'; import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_utils.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/ws_connect_stat.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/utils/json_converter.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/main/upload_stat_request.dart'; import 'package:sendbird_chat_sdk/src/internal/network/websocket/event/login_event.dart'; import 'package:sendbird_chat_sdk/src/public/main/define/exceptions.dart'; @@ -22,14 +24,18 @@ class StatManager { 'allow_sdk_noti_stats_log_publish': {StatType.notificationStats}, }; static const int _errStatUploadNotAllowed = 403200; + static const int _initialMinStatCount = 100; final Chat _chat; final int _maxStatCountPerRequest = 1000; - final int _minStatCount = 100; + int _minStatCount = _initialMinStatCount; + final int _intervalCountToTryAgain = 20; final int _minInterval = const Duration(hours: 3).inMilliseconds; final int _lowerThreshold = 10; StatState state = StatState.pending; + bool _isLoaded = false; + Future _setState(value) async { state = value; switch (value) { @@ -51,12 +57,10 @@ class StatManager { final List cachedDefaultStats = []; final DefaultStatPrefs defaultStatPrefs; - bool _isLoaded = false; bool _isFlushing = false; - CancelableOperation? _appendPendingDefaultStatsOperation; - CancelableOperation? _doAppendStatOperation; - CancelableOperation? _sendStatsOperation; + final Map _apiResultStartTsMap = {}; + final Map _wsConnectStartTsMap = {}; StatManager({required Chat chat}) : _chat = chat, @@ -68,19 +72,16 @@ class StatManager { required Map data, }) async { sbLog.d(StackTrace.current, 'type: $type'); - - _doAppendStatOperation = CancelableOperation.fromFuture( - _doAppendStat(type: type, data: data), - ); - return await _doAppendStatOperation?.value; + return await _doAppendStat(type: type, data: data); } + //- Public Future _doAppendStat({ required String type, required Map data, }) async { - sbLog.d(StackTrace.current, 'type: $type'); + sbLog.d(StackTrace.current); bool result = false; final statType = StatUtils.getStatType(type); @@ -91,18 +92,6 @@ class StatManager { result = await _append(stat); } } - - if (result) { - if (stat is NotificationStat) { - sbLog.i( - StackTrace.current, - 'type: $type, _state: $state, result: $result, stat: ${stat.toJson().toString()}', - ); - } - } else { - sbLog.w( - StackTrace.current, 'type: $type, _state: $state, result: $result'); - } return result; } @@ -111,7 +100,15 @@ class StatManager { switch (statType) { case StatType.apiResult: + return ApiResultStat.fromJson( + ts: DateTime.now().millisecondsSinceEpoch, + data: data, + ); case StatType.wsConnect: + return WsConnectStat.fromJson( + ts: DateTime.now().millisecondsSinceEpoch, + data: data, + ); case StatType.featureLocalCache: return null; case StatType.notificationStats: @@ -136,14 +133,23 @@ class StatManager { } Future _doAppend(BaseStat stat) async { - sbLog.d(StackTrace.current); + bool result = false; switch (state) { case StatState.pending: if (stat is DefaultStat) { pendingDefaultStats.add(stat); await defaultStatPrefs.appendStat(stat); - return true; + + sbLog.d( + StackTrace.current, + '[StatTest][Append] state: $state' + ' pendingDefaultStats: ${pendingDefaultStats.length},' + ' cachedDefaultStats: ${cachedDefaultStats.length},' + ' defaultStatPrefs: ${await defaultStatPrefs.statCount},' + ' stat: \n${jsonEncoder.convert(stat.toJson())}'); + + result = true; } break; case StatState.enabled: @@ -152,21 +158,27 @@ class StatManager { cachedDefaultStats.add(stat); await defaultStatPrefs.appendStat(stat); - await _checkToSendStats(); - return true; + sbLog.d( + StackTrace.current, + '[StatTest][Append] state: $state' + ' pendingDefaultStats: ${pendingDefaultStats.length},' + ' cachedDefaultStats: ${cachedDefaultStats.length},' + ' defaultStatPrefs: ${await defaultStatPrefs.statCount},' + ' stat: \n${jsonEncoder.convert(stat.toJson())}'); + + await _checkToSendStats(stat); + result = true; } break; case StatState.disabled: break; } - return false; + return result; } - Future _checkToSendStats() async { - sbLog.d(StackTrace.current); - + Future _checkToSendStats(BaseStat stat) async { final count = cachedDefaultStats.length; - sbLog.d(StackTrace.current, 'count: $count'); + sbLog.d(StackTrace.current, 'cachedDefaultStats: $count'); if (state == StatState.enabled && count >= _lowerThreshold) { final lastSentAt = await defaultStatPrefs.lastSentAt; @@ -174,11 +186,17 @@ class StatManager { sbLog.d(StackTrace.current, 'interval(sec): ${interval / 1000}'); final canSendRegardingInterval = (interval > _minInterval); - final canSendRegardingCount = (count == _minStatCount || - (count > _minStatCount && count % 20 == 0)); + final canSendRegardingCount = (count >= _minStatCount); if (canSendRegardingInterval || canSendRegardingCount) { - _sendStatsOperation = CancelableOperation.fromFuture(_sendStats()); + if (stat is ApiResultStat && + stat.endpoint.contains(UploadStatRequest.statUrl)) { + // Defensive code + sbLog.w(StackTrace.current, 'Ignored the stat for statistics API'); + return; + } + + await _sendStats(); } } } @@ -202,6 +220,8 @@ class StatManager { UploadStatRequest(_chat, deviceId: deviceId, stats: copiedStats), ); } catch (e) { + _minStatCount += _intervalCountToTryAgain; + exception = e; if (e is SendbirdException && e.code == _errStatUploadNotAllowed) { sbLog.w(StackTrace.current, 'errStatUploadNotAllowed: 403200'); @@ -212,8 +232,7 @@ class StatManager { } if (exception == null) { - sbLog.i(StackTrace.current, - '[Sent] deviceId: $deviceId, count: ${copiedStats.length}'); + _minStatCount = _initialMinStatCount; final List remainingStats = []; try { @@ -229,6 +248,13 @@ class StatManager { await defaultStatPrefs .updateLastSentAt(DateTime.now().millisecondsSinceEpoch); await defaultStatPrefs.putStats(remainingStats); + + sbLog.d( + StackTrace.current, + '[StatTest][Sent] deviceId: $deviceId,' + ' pendingDefaultStats: ${pendingDefaultStats.length},' + ' cachedDefaultStats: ${cachedDefaultStats.length},' + ' defaultStatPrefs: ${await defaultStatPrefs.statCount}'); } _isFlushing = false; @@ -254,43 +280,30 @@ class StatManager { sbLog.d(StackTrace.current); await _setState(StatState.disabled); } + //- EventDispatcher Future _onStatOn() async { sbLog.d(StackTrace.current); - await _loadOnce(); - - _appendPendingDefaultStatsOperation = - CancelableOperation.fromFuture(_appendPendingDefaultStats()); - } - - Future _loadOnce() async { - if (_isLoaded) return; - - pendingDefaultStats.addAll(await defaultStatPrefs.stats); - sbLog.d(StackTrace.current, - '_pendingDefaultStats.length: ${pendingDefaultStats.length}'); - - _isLoaded = true; - } - - Future _appendPendingDefaultStats() async { - sbLog.d(StackTrace.current); - - for (final stat in pendingDefaultStats) { - await _append(stat); + if (_isLoaded == false) { + pendingDefaultStats.addAll(await defaultStatPrefs.stats); + _isLoaded = true; } + + cachedDefaultStats.addAll(pendingDefaultStats); pendingDefaultStats.clear(); + + sbLog.d( + StackTrace.current, + '[StatTest][StatOn] ' + ' pendingDefaultStats: ${pendingDefaultStats.length},' + ' cachedDefaultStats: ${cachedDefaultStats.length},' + ' defaultStatPrefs: ${await defaultStatPrefs.statCount}'); } Future _onStatOff() async { sbLog.d(StackTrace.current); - - await _appendPendingDefaultStatsOperation?.cancel(); - await _doAppendStatOperation?.cancel(); - await _sendStatsOperation?.cancel(); - await _clearAll(); } @@ -358,4 +371,86 @@ class StatManager { sbLog.d(StackTrace.current, 'result: $result'); return result; } + + //+ WsConnectStat + void startWsConnectStat({ + required String hostUrl, + }) { + sbLog.d(StackTrace.current); + + if (_wsConnectStartTsMap[hostUrl] == null) { + _wsConnectStartTsMap[hostUrl] = DateTime.now().millisecondsSinceEpoch; + } + } + + void endWsConnectStat({ + required String hostUrl, + required bool success, + int? errorCode, + String? errorDescription, + }) async { + sbLog.d(StackTrace.current); + + final startTs = _wsConnectStartTsMap[hostUrl]; + if (startTs == null) return; + + final latency = DateTime.now().millisecondsSinceEpoch - startTs; + + await appendStat( + type: StatUtils.getStatTypeString(StatType.wsConnect), + data: { + 'host_url': hostUrl, + 'success': success, + 'latency': latency, + 'error_code': errorCode, + 'error_description': errorDescription, + }, + ); + + _wsConnectStartTsMap.remove(hostUrl); + } + + //- WsConnectStat + + //+ ApiResultStat + void startApiResultStat({ + required String endpoint, + }) { + sbLog.d(StackTrace.current); + + if (_wsConnectStartTsMap[endpoint] == null) { + _apiResultStartTsMap[endpoint] = DateTime.now().millisecondsSinceEpoch; + } + } + + void endApiResultStat({ + required String endpoint, + required String method, + required bool success, + int? errorCode, + String? errorDescription, + }) async { + sbLog.d(StackTrace.current); + + final startTs = _apiResultStartTsMap[endpoint]; + if (startTs == null) return; + + final latency = DateTime.now().millisecondsSinceEpoch - startTs; + + await appendStat( + type: StatUtils.getStatTypeString(StatType.apiResult), + data: { + 'endpoint': endpoint, + 'method': method, + 'success': success, + 'latency': latency, + 'error_code': errorCode, + 'error_description': errorDescription, + }, + ); + + _apiResultStartTsMap.remove(endpoint); + } + +//- ApiResultStat } diff --git a/lib/src/internal/main/stats/ws_connect_stat.dart b/lib/src/internal/main/stats/ws_connect_stat.dart new file mode 100644 index 00000000..f3da3eeb --- /dev/null +++ b/lib/src/internal/main/stats/ws_connect_stat.dart @@ -0,0 +1,76 @@ +// Copyright (c) 2023 Sendbird, Inc. All rights reserved. + +import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/default_stat.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_type.dart'; + +class WsConnectStat extends DefaultStat { + final String hostUrl; // ws host url + final bool success; // success or not + final int latency; // roundtrip latency + final int? errorCode; // error code if exist + final String? errorDescription; // detailed error message + + WsConnectStat({ + required int ts, + required this.hostUrl, + required this.success, + required this.latency, + this.errorCode, + this.errorDescription, + }) : super(StatType.wsConnect, ts); + + @override + Map toJson() { + final json = { + 'host_url': hostUrl, + 'success': success, + 'latency': latency, + 'error_code': errorCode, + 'error_description': errorDescription, + }; + final result = super.toJson(); + result['data'] = json; + return result; + } + + // { + // 'stat_type' : 'ws:connect', + // 'ts': int, // timestamp for log creation, + // 'data' : { + // 'host_url': String, + // 'success': bool, + // 'latency': int, + // 'error_code': int?, + // 'error_description': String?, + // }, + // } + static WsConnectStat? fromJson({ + required int ts, + required Map data, + }) { + try { + final String? hostUrl = data['host_url'] as String?; + final bool? success = data['success'] as bool?; + final int? latency = data['latency'] as int?; + final int? errorCode = data['error_code'] as int?; + final String? errorDescription = data['error_description'] as String?; + + if (hostUrl == null || success == null || latency == null) { + return null; + } + + return WsConnectStat( + ts: ts, + hostUrl: hostUrl, + success: success, + latency: latency, + errorCode: errorCode, + errorDescription: errorDescription, + ); + } catch (e) { + sbLog.d(StackTrace.current, 'e: ${e.toString()}'); + } + return null; + } +} diff --git a/lib/src/internal/network/http/api_client.dart b/lib/src/internal/network/http/api_client.dart index 57928bb0..7dfb2e8b 100644 --- a/lib/src/internal/network/http/api_client.dart +++ b/lib/src/internal/network/http/api_client.dart @@ -5,19 +5,24 @@ import 'dart:async'; import 'package:sendbird_chat_sdk/src/internal/main/chat_context/chat_context.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/session_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/http_client.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/api_request.dart'; import 'package:sendbird_chat_sdk/src/public/main/define/exceptions.dart'; class ApiClient { + final StatManager? _statManager; final HttpClient _httpClient; ApiClient({ required ChatContext chatContext, required SessionManager sessionManager, - }) : _httpClient = HttpClient( + StatManager? statManager, + }) : _statManager = statManager, + _httpClient = HttpClient( chatContext: chatContext, sessionManager: sessionManager, + statManager: statManager, ); void cleanUp() { @@ -31,8 +36,13 @@ class ApiClient { // possible other solution T is return type Future send(ApiRequest request) async { + final url = '/${request.version}/${request.url}'; + final uri = _httpClient.toUri( + url, + queryParams: request.queryParams, + ); + try { - final url = '/${request.version}/${request.url}'; // inject current user id if field is empty when only needs // need to clarify the condition if (request.method != HttpMethod.get && @@ -46,7 +56,7 @@ class ApiClient { switch (request.method) { case HttpMethod.get: json = await _httpClient.get( - url: url, + uri: uri, queryParams: request.queryParams, headers: request.headers, ); @@ -55,7 +65,7 @@ class ApiClient { case HttpMethod.post: if (!request.isMultipart) { json = await _httpClient.post( - url: url, + uri: uri, queryParams: request.queryParams, body: request.body, headers: request.headers, @@ -64,9 +74,9 @@ class ApiClient { } else { json = await _httpClient.requestMultipart( method: request.method, - url: url, - body: request.body, + uri: uri, queryParams: request.queryParams, + body: request.body, headers: request.headers, progressHandler: request.progressHandler, ); @@ -76,7 +86,7 @@ class ApiClient { case HttpMethod.put: if (!request.isMultipart) { json = await _httpClient.put( - url: url, + uri: uri, queryParams: request.queryParams, body: request.body, headers: request.headers, @@ -84,9 +94,9 @@ class ApiClient { } else { json = await _httpClient.requestMultipart( method: request.method, - url: url, - body: request.body, + uri: uri, queryParams: request.queryParams, + body: request.body, headers: request.headers, progressHandler: request.progressHandler, ); @@ -95,31 +105,48 @@ class ApiClient { case HttpMethod.delete: json = await _httpClient.delete( - url: url, + uri: uri, queryParams: request.queryParams, body: request.body, headers: request.headers, ); break; - - // case HttpMethod.patch: - // json = await _httpClient.patch( - // url: url, - // queryParams: request.queryParams, - // body: request.body, - // headers: request.headers, - // ); - // break; } final res = await request.response(json); + + _statManager?.endApiResultStat( + endpoint: uri.toString(), + method: request.method.name.toUpperCase(), + success: true, + ); + return res as T; } catch (e) { sbLog.e(StackTrace.current, 'e: $e'); + if (e is SendbirdException) { + _statManager?.endApiResultStat( + endpoint: uri.toString(), + method: request.method.name.toUpperCase(), + success: false, + errorCode: e.code, + errorDescription: e.message, + ); + rethrow; } else { - throw RequestFailedException(message: e.toString()); + final exception = RequestFailedException(message: e.toString()); + + _statManager?.endApiResultStat( + endpoint: uri.toString(), + method: request.method.name.toUpperCase(), + success: false, + errorCode: exception.code, + errorDescription: exception.message, + ); + + throw exception; } } } diff --git a/lib/src/internal/network/http/http_client/http_client.dart b/lib/src/internal/network/http/http_client/http_client.dart index 826cf1b9..312208f4 100644 --- a/lib/src/internal/network/http/http_client/http_client.dart +++ b/lib/src/internal/network/http/http_client/http_client.dart @@ -9,6 +9,7 @@ import 'package:sendbird_chat_sdk/src/internal/main/chat_context/chat_context.da import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/session_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/extensions/extensions.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/stats/stat_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/utils/json_converter.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/multipart_request.dart'; import 'package:sendbird_chat_sdk/src/public/core/channel/base_channel/base_channel.dart'; @@ -21,7 +22,6 @@ enum HttpMethod { post, put, delete, - // patch, } class HttpClient { @@ -31,12 +31,15 @@ class HttpClient { final ChatContext _chatContext; final SessionManager _sessionManager; + final StatManager? _statManager; HttpClient({ required ChatContext chatContext, required SessionManager sessionManager, + StatManager? statManager, }) : _chatContext = chatContext, - _sessionManager = sessionManager; + _sessionManager = sessionManager, + _statManager = statManager; ChatContext get chatContext => _chatContext; @@ -46,22 +49,31 @@ class HttpClient { errorStreamController = null; } - Future get({ - required String url, + Uri toUri( + String url, { Map? queryParams, - Map? headers, - }) async { - final uri = Uri( + }) { + return Uri( scheme: 'https', host: _chatContext.apiHost, path: url, queryParameters: _convertQueryParams(queryParams), ); + } + Future get({ + required Uri uri, + Map? queryParams, + Map? headers, + }) async { final request = http.Request('GET', uri); request.headers.addAll(_commonHeaders()); request.headers.addAll(headers ?? {}); + _statManager?.startApiResultStat( + endpoint: uri.toString(), + ); + sbLog.d(StackTrace.current, '\n-[url] $uri\n-[headers] ${jsonEncoder.convert(request.headers)}\n-[queryParams] ${jsonEncoder.convert(queryParams)}'); @@ -76,19 +88,12 @@ class HttpClient { } Future post({ - required String url, + required Uri uri, Map queryParams = const {}, Map body = const {}, Map headers = const {}, bool? isAuthenticateFeed, }) async { - final uri = Uri( - scheme: 'https', - host: _chatContext.apiHost, - path: url, - queryParameters: _convertQueryParams(queryParams), - ); - final request = http.Request('POST', uri); request.body = jsonEncode(body); request.headers.addAll(_commonHeaders( @@ -96,6 +101,10 @@ class HttpClient { )); request.headers.addAll(headers); + _statManager?.startApiResultStat( + endpoint: uri.toString(), + ); + sbLog.d(StackTrace.current, '\n-[url] $uri\n-[headers] ${jsonEncoder.convert(request.headers)}\n-[queryParams] ${jsonEncoder.convert(queryParams)}\n-[body] ${jsonEncoder.convert(body)}'); @@ -110,23 +119,20 @@ class HttpClient { } Future put({ - required String url, + required Uri uri, Map queryParams = const {}, Map body = const {}, Map headers = const {}, }) async { - final uri = Uri( - scheme: 'https', - host: _chatContext.apiHost, - path: url, - queryParameters: _convertQueryParams(queryParams), - ); - final request = http.Request('PUT', uri); request.body = jsonEncode(body); request.headers.addAll(_commonHeaders()); request.headers.addAll(headers); + _statManager?.startApiResultStat( + endpoint: uri.toString(), + ); + sbLog.d(StackTrace.current, '\n-[url] $uri\n-[headers] ${jsonEncoder.convert(request.headers)}\n-[queryParams] ${jsonEncoder.convert(queryParams)}\n-[body] ${jsonEncoder.convert(body)}'); @@ -141,23 +147,20 @@ class HttpClient { } Future delete({ - required String url, + required Uri uri, Map queryParams = const {}, Map body = const {}, Map headers = const {}, }) async { - final uri = Uri( - scheme: 'https', - host: _chatContext.apiHost, - path: url, - queryParameters: _convertQueryParams(queryParams), - ); - final request = http.Request('DELETE', uri); request.headers.addAll(_commonHeaders()); request.body = jsonEncode(body); request.headers.addAll(headers); + _statManager?.startApiResultStat( + endpoint: uri.toString(), + ); + sbLog.d(StackTrace.current, '\n-[url] $uri\n-[headers] ${jsonEncoder.convert(request.headers)}\n-[queryParams] ${jsonEncoder.convert(queryParams)}\n-[body] ${jsonEncoder.convert(body)}'); @@ -171,51 +174,14 @@ class HttpClient { return _response(res); } - // Future patch({ - // required String url, - // Map? queryParams, - // Map? body, - // Map? headers, - // }) async { - // final uri = Uri( - // scheme: 'https', - // host: _chatContext.apiHost, - // path: url, - // queryParameters: _convertQueryParams(queryParams), - // ); - // - // final request = http.Request('PATCH', uri); - // request.headers.addAll(_commonHeaders()); - // request.headers.addAll(headers ?? {}); - // - // sbLog.d(StackTrace.current, - // '\n-[url] $uri\n-[headers] ${jsonEncoder.convert(request.headers)}\n-[queryParams] ${jsonEncoder.convert(queryParams)}\n-[body] ${jsonEncoder.convert(body)}'); - // - // http.Response res = await http.Response.fromStream(await request.send()); - // if (await _checkSessionKeyExpired(res)) { - // final secondRequest = _copyRequest(request); - // if (secondRequest != null) { - // res = await http.Response.fromStream(await secondRequest.send()); - // } - // } - // return _response(res); - // } - Future requestMultipart({ required HttpMethod method, - required String url, - Map? body, + required Uri uri, Map? queryParams, + Map? body, Map? headers, ProgressHandler? progressHandler, }) async { - final uri = Uri( - scheme: 'https', - host: _chatContext.apiHost, - path: url, - queryParameters: _convertQueryParams(queryParams ?? {}), - ); - final request = MultipartRequest( method.asString().toUpperCase(), uri, @@ -349,23 +315,6 @@ class HttpClient { } } - Map _convertQueryParams(Map? q) { - if (q == null) return {}; - final result = {}; - q.forEach((key, value) { - if (value is List) { - if (value is List) { - result[key] = value; - } else { - result[key] = value.map((e) => e.toString()).toList(); - } - } else if (value != null) { - result[key] = value.toString(); - } - }); - return result; - } - Future _checkSessionKeyExpired(http.Response response) async { dynamic body; @@ -417,4 +366,21 @@ class HttpClient { ..headers.addAll(_commonHeaders()); // Apply updated sessionKey return requestCopy; } + + Map _convertQueryParams(Map? q) { + if (q == null) return {}; + final result = {}; + q.forEach((key, value) { + if (value is List) { + if (value is List) { + result[key] = value; + } else { + result[key] = value.map((e) => e.toString()).toList(); + } + } else if (value != null) { + result[key] = value.toString(); + } + }); + return result; + } } diff --git a/lib/src/internal/network/http/http_client/request/main/upload_stat_request.dart b/lib/src/internal/network/http/http_client/request/main/upload_stat_request.dart index 5698d6e3..fb77394b 100644 --- a/lib/src/internal/network/http/http_client/request/main/upload_stat_request.dart +++ b/lib/src/internal/network/http/http_client/request/main/upload_stat_request.dart @@ -9,12 +9,14 @@ class UploadStatRequest extends ApiRequest { @override HttpMethod get method => HttpMethod.post; + static const statUrl = 'sdk/statistics'; + UploadStatRequest( Chat chat, { required String deviceId, required List stats, }) : super(chat: chat) { - url = 'sdk/statistics'; + url = statUrl; body = { 'device_id': deviceId, 'log_entries': stats.map((stat) => stat.toJson()).toList(), diff --git a/lib/src/public/core/message/base_message.dart b/lib/src/public/core/message/base_message.dart index 03a6c411..faf04802 100644 --- a/lib/src/public/core/message/base_message.dart +++ b/lib/src/public/core/message/base_message.dart @@ -525,9 +525,20 @@ abstract class BaseMessage { json['is_reply_to_channel'] = json['reply_to_channel']; } - BaseMessage message; - final type = commandType ?? json['type'] as String; + String? type = commandType; + if (type == null) { + final Map? file = json['file']; + if (file != null && file.isNotEmpty) { + // The 'type' value of FileMessage payload can be 'FILE' or 'image/jpeg'. + type = CommandType.fileMessage.value; + } else { + type = json['type'] as String; + } + } + + // final type = commandType ?? json['type'] as String; + BaseMessage message; if (chat != null) { if (T == UserMessage || CommandString.isUserMessage(type)) { message = UserMessage.fromJsonWithChat(chat, json) as T; diff --git a/pubspec.yaml b/pubspec.yaml index 7b3eefce..5d229d10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sendbird_chat_sdk description: With Sendbird Chat for Flutter, you can easily build an in-app chat with all the essential messaging features. -version: 4.0.11 +version: 4.0.12 homepage: https://sendbird.com repository: https://github.com/sendbird/sendbird-chat-sdk-flutter documentation: https://sendbird.com/docs/chat/sdk/v4/flutter/getting-started/send-first-message