Skip to content

Commit 9c6f04b

Browse files
authored
refactor(llc, core, ui): update X-Stream-Client header for enhanced client identification (#2131)
* refactor(llc, core, ui): update X-Stream-Client header for enhanced client identification * chore: update CHANGELOG.md * chore: fix analysis * fix: downgrade device_info_plus for lower flutter versions * test: fix tests * chore: remove extra methods * refactor: append CurrentPlatform in default SystemEnvironment * fix: fix test * chore: fix lint
1 parent 5d654ab commit 9c6f04b

19 files changed

+526
-56
lines changed

melos.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ command:
3232
contextmenu: ^3.0.0
3333
cupertino_icons: ^1.0.3
3434
desktop_drop: ^0.5.0
35+
device_info_plus: ^11.3.0
3536
diacritic: ^0.1.5
3637
dio: ^5.4.3+1
3738
drift: ^2.22.1
@@ -65,6 +66,7 @@ command:
6566
media_kit_video: ^1.2.4
6667
meta: ^1.9.1
6768
mime: ^2.0.0
69+
package_info_plus: ^8.3.0
6870
path: ^1.8.3
6971
path_provider: ^2.1.3
7072
photo_manager: ^3.2.0

packages/stream_chat/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
🔄 Changed
1313

1414
- Refactored identifying the `Attachment.uploadState` logic for local and remote attachments. Also updated the logic for determining the attachment type to check for ogScrapeUrl instead of `AttachmentType.giphy`.
15+
- Improved the `x-stream-client` header generation for better client identification and analytics.
1516

1617
✅ Added
1718

packages/stream_chat/lib/src/client/client.dart

+27-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// ignore_for_file: unnecessary_getters_setters
2-
31
import 'dart:async';
42

53
import 'package:dio/dio.dart';
@@ -14,6 +12,7 @@ import 'package:stream_chat/src/core/api/stream_chat_api.dart';
1412
import 'package:stream_chat/src/core/error/error.dart';
1513
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
1614
import 'package:stream_chat/src/core/http/stream_http_client.dart';
15+
import 'package:stream_chat/src/core/http/system_environment_manager.dart';
1716
import 'package:stream_chat/src/core/http/token.dart';
1817
import 'package:stream_chat/src/core/http/token_manager.dart';
1918
import 'package:stream_chat/src/core/models/attachment_file.dart';
@@ -27,10 +26,10 @@ import 'package:stream_chat/src/core/models/poll.dart';
2726
import 'package:stream_chat/src/core/models/poll_option.dart';
2827
import 'package:stream_chat/src/core/models/poll_vote.dart';
2928
import 'package:stream_chat/src/core/models/user.dart';
30-
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
3129
import 'package:stream_chat/src/core/util/utils.dart';
3230
import 'package:stream_chat/src/db/chat_persistence_client.dart';
3331
import 'package:stream_chat/src/event_type.dart';
32+
import 'package:stream_chat/src/system_environment.dart';
3433
import 'package:stream_chat/src/ws/connection_status.dart';
3534
import 'package:stream_chat/src/ws/websocket.dart';
3635
import 'package:stream_chat/version.dart';
@@ -85,7 +84,6 @@ class StreamChatClient {
8584
baseUrl: baseURL,
8685
connectTimeout: connectTimeout,
8786
receiveTimeout: receiveTimeout,
88-
headers: {'X-Stream-Client': defaultUserAgent},
8987
);
9088

9189
_chatApi = chatApi ??
@@ -94,6 +92,7 @@ class StreamChatClient {
9492
options: options,
9593
tokenManager: _tokenManager,
9694
connectionIdManager: _connectionIdManager,
95+
systemEnvironmentManager: _systemEnvironmentManager,
9796
attachmentFileUploaderProvider: attachmentFileUploaderProvider,
9897
logger: detachedLogger('🕸️'),
9998
interceptors: chatApiInterceptors,
@@ -105,11 +104,9 @@ class StreamChatClient {
105104
apiKey: apiKey,
106105
baseUrl: baseWsUrl ?? options.baseUrl,
107106
tokenManager: _tokenManager,
107+
systemEnvironmentManager: _systemEnvironmentManager,
108108
handler: handleEvent,
109109
logger: detachedLogger('🔌'),
110-
queryParameters: {
111-
'X-Stream-Client': '$defaultUserAgent-$packageVersion',
112-
},
113110
);
114111

115112
_retryPolicy = retryPolicy ??
@@ -130,10 +127,31 @@ class StreamChatClient {
130127

131128
final _tokenManager = TokenManager();
132129
final _connectionIdManager = ConnectionIdManager();
130+
static final _systemEnvironmentManager = SystemEnvironmentManager();
131+
132+
/// Updates the system environment information used by the client.
133+
///
134+
/// It allows you to set environment-specific information that will be
135+
/// included in API requests, such as the application name, platform details,
136+
/// and version information.
137+
///
138+
/// Example:
139+
/// ```dart
140+
/// client.updateSystemEnvironment(
141+
/// SystemEnvironment(
142+
/// name: 'my_app',
143+
/// version: '1.0.0',
144+
/// ),
145+
/// );
146+
/// ```
147+
///
148+
/// See [SystemEnvironment] for more information on the available fields.
149+
void updateSystemEnvironment(SystemEnvironment environment) {
150+
_systemEnvironmentManager.updateEnvironment(environment);
151+
}
133152

134153
/// Default user agent for all requests
135-
static String defaultUserAgent =
136-
'stream-chat-dart-client-${CurrentPlatform.name}';
154+
static String defaultUserAgent = _systemEnvironmentManager.userAgent;
137155

138156
/// Additional headers for all requests
139157
static Map<String, Object?> additionalHeaders = {};

packages/stream_chat/lib/src/core/api/stream_chat_api.dart

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:stream_chat/src/core/api/threads_api.dart';
1313
import 'package:stream_chat/src/core/api/user_api.dart';
1414
import 'package:stream_chat/src/core/http/connection_id_manager.dart';
1515
import 'package:stream_chat/src/core/http/stream_http_client.dart';
16+
import 'package:stream_chat/src/core/http/system_environment_manager.dart';
1617
import 'package:stream_chat/src/core/http/token_manager.dart';
1718

1819
export 'device_api.dart' show PushProvider;
@@ -26,6 +27,7 @@ class StreamChatApi {
2627
StreamHttpClientOptions? options,
2728
TokenManager? tokenManager,
2829
ConnectionIdManager? connectionIdManager,
30+
SystemEnvironmentManager? systemEnvironmentManager,
2931
AttachmentFileUploaderProvider attachmentFileUploaderProvider =
3032
StreamAttachmentFileUploader.new,
3133
Logger? logger,
@@ -38,6 +40,7 @@ class StreamChatApi {
3840
options: options,
3941
tokenManager: tokenManager,
4042
connectionIdManager: connectionIdManager,
43+
systemEnvironmentManager: systemEnvironmentManager,
4144
logger: logger,
4245
interceptors: interceptors,
4346
httpClientAdapter: httpClientAdapter,

packages/stream_chat/lib/src/core/http/interceptor/additional_headers_interceptor.dart

+9
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import 'package:dio/dio.dart';
2+
import 'package:stream_chat/src/core/http/system_environment_manager.dart';
23
import 'package:stream_chat/stream_chat.dart';
34

45
/// Interceptor that sets additional headers for all requests.
56
class AdditionalHeadersInterceptor extends Interceptor {
7+
/// Initialize a new [AdditionalHeadersInterceptor].
8+
const AdditionalHeadersInterceptor([this._systemEnvironmentManager]);
9+
10+
final SystemEnvironmentManager? _systemEnvironmentManager;
11+
612
@override
713
Future<void> onRequest(
814
RequestOptions options,
915
RequestInterceptorHandler handler,
1016
) async {
17+
final userAgent = _systemEnvironmentManager?.userAgent;
18+
1119
options.headers = {
1220
...options.headers,
1321
...StreamChatClient.additionalHeaders,
22+
if (userAgent != null) 'X-Stream-Client': userAgent,
1423
};
1524
return handler.next(options);
1625
}

packages/stream_chat/lib/src/core/http/stream_http_client.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:stream_chat/src/core/http/interceptor/auth_interceptor.dart';
1010
import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor.dart';
1111
import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart';
1212
import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart';
13+
import 'package:stream_chat/src/core/http/system_environment_manager.dart';
1314
import 'package:stream_chat/src/core/http/token_manager.dart';
1415

1516
part 'stream_http_client_options.dart';
@@ -24,6 +25,7 @@ class StreamHttpClient {
2425
StreamHttpClientOptions? options,
2526
TokenManager? tokenManager,
2627
ConnectionIdManager? connectionIdManager,
28+
SystemEnvironmentManager? systemEnvironmentManager,
2729
Logger? logger,
2830
Iterable<Interceptor>? interceptors,
2931
HttpClientAdapter? httpClientAdapter,
@@ -43,7 +45,7 @@ class StreamHttpClient {
4345
..._options.headers,
4446
}
4547
..interceptors.addAll([
46-
AdditionalHeadersInterceptor(),
48+
AdditionalHeadersInterceptor(systemEnvironmentManager),
4749
if (tokenManager != null) AuthInterceptor(this, tokenManager),
4850
if (connectionIdManager != null)
4951
ConnectionIdInterceptor(connectionIdManager),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// ignore_for_file: use_setters_to_change_properties
2+
3+
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
4+
import 'package:stream_chat/src/system_environment.dart';
5+
import 'package:stream_chat/version.dart';
6+
7+
/// {@template systemEnvironmentManager}
8+
/// A manager class to handle the current [SystemEnvironment].
9+
/// {@endtemplate}
10+
class SystemEnvironmentManager {
11+
/// {@macro systemEnvironmentManager}
12+
SystemEnvironmentManager({
13+
SystemEnvironment? environment,
14+
}) : _environment = switch (environment) {
15+
final env? => env,
16+
_ => SystemEnvironment(
17+
sdkName: 'stream-chat',
18+
sdkIdentifier: 'dart',
19+
sdkVersion: PACKAGE_VERSION,
20+
osName: CurrentPlatform.name,
21+
),
22+
};
23+
24+
/// Returns the Stream client user agent string based on the current
25+
/// [environment] value.
26+
String get userAgent => _environment.xStreamClientHeader;
27+
28+
/// The current [SystemEnvironment].
29+
SystemEnvironment get environment => _environment;
30+
SystemEnvironment _environment;
31+
32+
/// Updates the current [SystemEnvironment].
33+
void updateEnvironment(SystemEnvironment environment) {
34+
_environment = environment;
35+
}
36+
}
37+
38+
/// Extension on [SystemEnvironment] to build a Stream client header string.
39+
extension XStreamClientHeaderExtension on SystemEnvironment {
40+
/// Builds a Stream client header string for API requests.
41+
///
42+
/// The header follows the format:
43+
/// `{sdk}-{identifier}-v{version}
44+
/// |app={appName}
45+
/// |app_version={appVersion}
46+
/// |os={osName} {osVersion}
47+
/// |device_model={deviceModel}`
48+
///
49+
/// Only non-null values are included in the header.
50+
String get xStreamClientHeader {
51+
final clientInfo = '$sdkName-$sdkIdentifier-v$sdkVersion';
52+
53+
return [
54+
clientInfo,
55+
if (appName case final name?) 'app=$name',
56+
if (appVersion case final version?) 'app_version=$version',
57+
switch ((osName, osVersion)) {
58+
(final name?, final version?) => 'os=$name $version',
59+
(final name?, null) => 'os=$name',
60+
_ => null,
61+
},
62+
if (deviceModel case final model?) 'device_model=$model',
63+
].nonNulls.join('|');
64+
}
65+
}

packages/stream_chat/lib/src/core/platform_detector/platform_detector.dart

+9-16
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,15 @@ class CurrentPlatform {
5656

5757
/// Returns a string version of the platform
5858
static String get name {
59-
switch (type) {
60-
case PlatformType.android:
61-
return 'android';
62-
case PlatformType.ios:
63-
return 'ios';
64-
case PlatformType.web:
65-
return 'web';
66-
case PlatformType.macOS:
67-
return 'macos';
68-
case PlatformType.windows:
69-
return 'windows';
70-
case PlatformType.linux:
71-
return 'linux';
72-
case PlatformType.fuchsia:
73-
return 'fuchsia';
74-
}
59+
return switch (type) {
60+
PlatformType.android => 'android',
61+
PlatformType.ios => 'ios',
62+
PlatformType.web => 'web',
63+
PlatformType.macOS => 'macos',
64+
PlatformType.windows => 'windows',
65+
PlatformType.linux => 'linux',
66+
PlatformType.fuchsia => 'fuchsia',
67+
};
7568
}
7669

7770
/// Get current platform type
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/// {@template streamChatSystemEnvironment}
2+
/// A class that represents the environment in which the Stream Chat SDK is
3+
/// running.
4+
///
5+
/// This class provides information about the SDK, application, and device
6+
/// used for tracking and debugging purposes.
7+
/// {@endtemplate}
8+
class SystemEnvironment {
9+
/// {@macro streamChatSystemEnvironment}
10+
const SystemEnvironment({
11+
required this.sdkName,
12+
required this.sdkIdentifier,
13+
required this.sdkVersion,
14+
this.appName,
15+
this.appVersion,
16+
this.osName,
17+
this.osVersion,
18+
this.deviceModel,
19+
});
20+
21+
/// The name of the SDK.
22+
final String sdkName;
23+
24+
/// The identifier of the SDK platform.
25+
///
26+
/// This is used to distinguish between different implementations
27+
/// (e.g., 'dart', 'flutter', etc.).
28+
final String sdkIdentifier;
29+
30+
/// The version of the SDK.
31+
final String sdkVersion;
32+
33+
/// The name of the application.
34+
///
35+
/// This is null by default and could be set by the application.
36+
final String? appName;
37+
38+
/// The version of the application.
39+
///
40+
/// This is null by default and could be set by the application.
41+
final String? appVersion;
42+
43+
/// The name of the operating system.
44+
///
45+
/// This is null by default and could be set by the application.
46+
final String? osName;
47+
48+
/// The version of the operating system.
49+
///
50+
/// This is null by default and could be set by the application.
51+
final String? osVersion;
52+
53+
/// The device model information.
54+
///
55+
/// This is null by default and could be set by the application.
56+
final String? deviceModel;
57+
}

packages/stream_chat/lib/src/ws/websocket.dart

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
66
import 'package:meta/meta.dart';
77
import 'package:rxdart/rxdart.dart';
88
import 'package:stream_chat/src/core/error/error.dart';
9+
import 'package:stream_chat/src/core/http/system_environment_manager.dart';
910
import 'package:stream_chat/src/core/http/token_manager.dart';
1011
import 'package:stream_chat/src/core/models/event.dart';
1112
import 'package:stream_chat/src/core/models/user.dart';
@@ -34,6 +35,7 @@ class WebSocket with TimerHelper {
3435
required this.apiKey,
3536
required this.baseUrl,
3637
required this.tokenManager,
38+
this.systemEnvironmentManager,
3739
this.handler,
3840
Logger? logger,
3941
this.webSocketChannelProvider,
@@ -52,9 +54,18 @@ class WebSocket with TimerHelper {
5254
/// WS base url
5355
final String baseUrl;
5456

57+
/// Manager responsible for handling authentication tokens.
5558
///
59+
/// Used to retrieve tokens for websocket connections and refreshing expired
60+
/// tokens.
5661
final TokenManager tokenManager;
5762

63+
/// Manager that provides system environment information like SDK version and
64+
/// platform details.
65+
///
66+
/// Used to send client identification in websocket connection requests.
67+
final SystemEnvironmentManager? systemEnvironmentManager;
68+
5869
/// Functions that will be called every time a new event is received from the
5970
/// connection
6071
final EventHandler? handler;
@@ -157,11 +168,14 @@ class WebSocket with TimerHelper {
157168
'user_token': token.rawValue,
158169
'server_determines_connection_id': true,
159170
};
171+
172+
final userAgent = systemEnvironmentManager?.userAgent;
160173
final qs = {
161174
'json': jsonEncode(params),
162175
'api_key': apiKey,
163176
'authorization': token.rawValue,
164177
'stream-auth-type': token.authType.name,
178+
if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent),
165179
...queryParameters,
166180
};
167181

packages/stream_chat/lib/stream_chat.dart

+1
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,5 @@ export 'src/core/util/extension.dart';
5757
export 'src/db/chat_persistence_client.dart';
5858
export 'src/event_type.dart';
5959
export 'src/permission_type.dart';
60+
export 'src/system_environment.dart';
6061
export 'src/ws/connection_status.dart';

0 commit comments

Comments
 (0)