Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

🔄 Changed

- `StreamChatClient.updateSystemEnvironment` now sanitizes the passed `SystemEnvironment`: `sdkName`, `sdkVersion`, and `osName` are locked to internal defaults, and `sdkIdentifier` only accepts the `dart` → `flutter` promotion (other values, including a `flutter` → `dart` demotion, are ignored). `appName`, `appVersion`, `osVersion`, and `deviceModel` continue to pass through as-is.

## 10.1.0

✅ Added
Expand Down
26 changes: 19 additions & 7 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,33 @@ class StreamChatClient {

/// Updates the system environment information used by the client.
///
/// It allows you to set environment-specific information that will be
/// included in API requests, such as the application name, platform details,
/// and version information.
/// The passed [environment] is sanitized before being applied:
///
/// Overridable fields (passed through as-is):
/// - [SystemEnvironment.appName]
/// - [SystemEnvironment.appVersion]
/// - [SystemEnvironment.osVersion]
/// - [SystemEnvironment.deviceModel]
///
/// Immutable fields (custom values are ignored, internal defaults are
/// preserved):
/// - [SystemEnvironment.sdkName]
/// - [SystemEnvironment.sdkIdentifier]
/// - [SystemEnvironment.sdkVersion]
/// - [SystemEnvironment.osName]
///
/// Example:
/// ```dart
/// client.updateSystemEnvironment(
/// SystemEnvironment(
/// name: 'my_app',
/// version: '1.0.0',
/// sdkName: 'stream-chat',
/// sdkIdentifier: 'dart',
/// sdkVersion: StreamChatClient.packageVersion,
/// appName: 'my_app',
/// appVersion: '1.0.0',
/// ),
Comment on lines +169 to 193

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the sdkIdentifier docs to match the sanitizer.

Line 180 documents sdkIdentifier as immutable, but SystemEnvironmentManager._sanitize accepts a dartflutter promotion and the new tests assert that behavior. The example also passes sdkName/sdkIdentifier/sdkVersion, which suggests those caller values matter when they are sanitized away or constrained.

Proposed doc update
-  /// Immutable fields (custom values are ignored, internal defaults are
-  /// preserved):
+  /// Internally controlled fields:
   /// - [SystemEnvironment.sdkName]
-  /// - [SystemEnvironment.sdkIdentifier]
   /// - [SystemEnvironment.sdkVersion]
   /// - [SystemEnvironment.osName]
+  ///
+  /// [SystemEnvironment.sdkIdentifier] keeps its current value unless the
+  /// update promotes it from `dart` to `flutter`.
@@
   /// client.updateSystemEnvironment(
   ///   SystemEnvironment(
-  ///     sdkName: 'stream-chat',
-  ///     sdkIdentifier: 'dart',
-  ///     sdkVersion: StreamChatClient.packageVersion,
   ///     appName: 'my_app',
   ///     appVersion: '1.0.0',
   ///   ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// The passed [environment] is sanitized before being applied:
///
/// Overridable fields (passed through as-is):
/// - [SystemEnvironment.appName]
/// - [SystemEnvironment.appVersion]
/// - [SystemEnvironment.osVersion]
/// - [SystemEnvironment.deviceModel]
///
/// Immutable fields (custom values are ignored, internal defaults are
/// preserved):
/// - [SystemEnvironment.sdkName]
/// - [SystemEnvironment.sdkIdentifier]
/// - [SystemEnvironment.sdkVersion]
/// - [SystemEnvironment.osName]
///
/// Example:
/// ```dart
/// client.updateSystemEnvironment(
/// SystemEnvironment(
/// name: 'my_app',
/// version: '1.0.0',
/// sdkName: 'stream-chat',
/// sdkIdentifier: 'dart',
/// sdkVersion: StreamChatClient.packageVersion,
/// appName: 'my_app',
/// appVersion: '1.0.0',
/// ),
/// The passed [environment] is sanitized before being applied:
///
/// Overridable fields (passed through as-is):
/// - [SystemEnvironment.appName]
/// - [SystemEnvironment.appVersion]
/// - [SystemEnvironment.osVersion]
/// - [SystemEnvironment.deviceModel]
///
/// Internally controlled fields:
/// - [SystemEnvironment.sdkName]
/// - [SystemEnvironment.sdkVersion]
/// - [SystemEnvironment.osName]
///
/// [SystemEnvironment.sdkIdentifier] keeps its current value unless the
/// update promotes it from `dart` to `flutter`.
///
/// Example:
///
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stream_chat/lib/src/client/client.dart` around lines 169 - 193, The
`SystemEnvironment` docs in `Client.updateSystemEnvironment` are inconsistent
with `SystemEnvironmentManager._sanitize`: `sdkIdentifier` is not fully
immutable because `dart` may be promoted to `flutter`. Update the documentation
around `updateSystemEnvironment` and the example to reflect the actual sanitizer
behavior, and make sure the `sdkIdentifier` bullet explains the allowed
normalization instead of claiming it is always preserved as-is.

/// );
/// ```
///
/// See [SystemEnvironment] for more information on the available fields.
void updateSystemEnvironment(SystemEnvironment environment) {
_systemEnvironmentManager.updateEnvironment(environment);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ class SystemEnvironmentManager {
/// {@macro systemEnvironmentManager}
SystemEnvironmentManager({
SystemEnvironment? environment,
}) : _environment = switch (environment) {
final env? => env,
_ => SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'dart',
sdkVersion: PACKAGE_VERSION,
osName: CurrentPlatform.name,
),
};
}) : _environment = SystemEnvironment(
sdkName: _sdkName,
sdkIdentifier: _SdkIdentifier.dart,
sdkVersion: PACKAGE_VERSION,
osName: CurrentPlatform.name,
) {
if (environment != null) updateEnvironment(environment);
}

static const _sdkName = 'stream-chat';

/// Returns the Stream client user agent string based on the current
/// [environment] value.
Expand All @@ -31,7 +32,37 @@ class SystemEnvironmentManager {

/// Updates the current [SystemEnvironment].
void updateEnvironment(SystemEnvironment environment) {
_environment = environment;
_environment = _sanitize(environment);
}

/// Sanitizes the passed [SystemEnvironment]
/// Ignores custom values for:
/// - sdkName
/// - sdkVersion
/// - osName
/// Allows only the dart -> flutter promotion for:
/// - sdkIdentifier (any other value, including a flutter -> dart
/// demotion, is ignored)
/// Allows overriding of:
/// - appName
/// - appVersion
/// - osVersion
/// - deviceModel
SystemEnvironment _sanitize(SystemEnvironment environment) {
final incoming = _SdkIdentifier(environment.sdkIdentifier);
final current = _SdkIdentifier(_environment.sdkIdentifier);
final sdkIdentifier = incoming.precedence < current.precedence ? current : incoming;
final osName = _environment.osName;
return SystemEnvironment(
sdkName: _sdkName,
sdkIdentifier: sdkIdentifier,
sdkVersion: PACKAGE_VERSION,
appName: environment.appName,
appVersion: environment.appVersion,
osName: osName,
osVersion: environment.osVersion,
deviceModel: environment.deviceModel,
);
}
}

Expand Down Expand Up @@ -63,3 +94,18 @@ extension XStreamClientHeaderExtension on SystemEnvironment {
].nonNulls.join('|');
}
}

/// Known SDK identifiers ranked by precedence. A proposed update is only
/// accepted when its precedence is greater than or equal to the current
/// identifier's, which makes the dart -> flutter transition one-way and
/// causes unknown identifiers to fall back to the current value.
extension type const _SdkIdentifier(String value) implements String {
static const dart = _SdkIdentifier('dart');
static const flutter = _SdkIdentifier('flutter');

int get precedence => switch (this) {
dart => 0,
flutter => 1,
_ => -1,
};
}
2 changes: 1 addition & 1 deletion packages/stream_chat/lib/src/ws/websocket.dart
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ class WebSocket with TimerHelper {
'api_key': apiKey,
'authorization': token.rawValue,
'stream-auth-type': token.authType.name,
if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent),
...queryParameters,
if (userAgent != null) 'X-Stream-Client': jsonEncode(userAgent),
};

final scheme = switch (baseUrl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,83 @@ void main() {
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
});

test('initializes with custom environment', () {
test('sanitizes environment passed to constructor', () {
const customEnv = SystemEnvironment(
sdkName: 'custom-sdk',
sdkIdentifier: 'custom',
sdkIdentifier: 'flutter',
sdkVersion: '1.0.0',
appName: 'test-app',
);

manager = SystemEnvironmentManager(environment: customEnv);

expect(manager.environment.sdkName, equals('custom-sdk'));
expect(manager.environment.sdkIdentifier, equals('custom'));
expect(manager.environment.sdkVersion, equals('1.0.0'));
// Immutable fields are forced to internal values.
expect(manager.environment.sdkName, equals('stream-chat'));
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
expect(manager.environment.osName, equals(CurrentPlatform.name));
// Whitelisted sdkIdentifier is accepted.
expect(manager.environment.sdkIdentifier, equals('flutter'));
// App fields are passed through.
expect(manager.environment.appName, equals('test-app'));
});

test('updates environment', () {
test('sanitizes environment on update', () {
const newEnv = SystemEnvironment(
sdkName: 'updated-sdk',
sdkIdentifier: 'updated',
sdkVersion: '2.0.0',
sdkName: 'stream-chat-android',
sdkIdentifier: 'android',
sdkVersion: '99.0.0',
appName: 'test-app',
appVersion: '2.0.0',
osName: 'spoofed-os',
osVersion: '14',
deviceModel: 'Pixel 7',
);

manager.updateEnvironment(newEnv);

expect(manager.environment.sdkName, equals('updated-sdk'));
expect(manager.environment.sdkIdentifier, equals('updated'));
expect(manager.environment.sdkVersion, equals('2.0.0'));
// Immutable fields are forced to internal values.
expect(manager.environment.sdkName, equals('stream-chat'));
expect(manager.environment.sdkVersion, equals(PACKAGE_VERSION));
expect(manager.environment.osName, equals(CurrentPlatform.name));
// Unknown sdkIdentifier falls back to the current value.
expect(manager.environment.sdkIdentifier, equals('dart'));
// App/device/os-version fields are passed through.
expect(manager.environment.appName, equals('test-app'));
expect(manager.environment.appVersion, equals('2.0.0'));
expect(manager.environment.osVersion, equals('14'));
expect(manager.environment.deviceModel, equals('Pixel 7'));
});

test('allows promoting sdkIdentifier from dart to flutter', () {
manager.updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'flutter',
sdkVersion: PACKAGE_VERSION,
),
);

expect(manager.environment.sdkIdentifier, equals('flutter'));
});

test('ignores demotion of sdkIdentifier from flutter to dart', () {
manager
..updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'flutter',
sdkVersion: PACKAGE_VERSION,
),
)
..updateEnvironment(
const SystemEnvironment(
sdkName: 'stream-chat',
sdkIdentifier: 'dart',
sdkVersion: PACKAGE_VERSION,
),
);

expect(manager.environment.sdkIdentifier, equals('flutter'));
});

test('userAgent returns proper header string', () {
Expand Down
Loading