Skip to content

Commit abbcfaf

Browse files
api: Embed platform and app info in user-agent
Generate the user-agent using `deviceInfo` and `packageInfo` from ZulipBinding. Fixes: #467
1 parent 894f9c3 commit abbcfaf

File tree

3 files changed

+70
-5
lines changed

3 files changed

+70
-5
lines changed

lib/model/binding.dart

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ abstract class ZulipBinding {
162162
AndroidNotificationHostApi get androidNotificationHost;
163163

164164
/// Generates a user agent header for HTTP requests.
165+
///
166+
/// Uses [deviceInfo] to get operating system information
167+
/// and [packageInfo] to get application version information.
165168
Map<String, String> userAgentHeader();
166169
}
167170

@@ -388,14 +391,40 @@ class LiveZulipBinding extends ZulipBinding {
388391

389392
@override
390393
Map<String, String> userAgentHeader() {
391-
return _userAgentHeader ??= buildUserAgentHeader();
394+
if (maybeDeviceInfo == null || maybePackageInfo == null) {
395+
assert(debugLog('userAgentHeader: Dependencies not initialized, falling back to \'ZulipFlutter\'.'));
396+
return {'User-Agent': 'ZulipFlutter'}; // TODO(log)
397+
}
398+
return _userAgentHeader ??= buildUserAgentHeader(maybeDeviceInfo!, maybePackageInfo!);
392399
}
393400
}
394401

395402
@visibleForTesting
396-
Map<String, String> buildUserAgentHeader() {
403+
Map<String, String> buildUserAgentHeader(BaseDeviceInfo deviceInfo, PackageInfo packageInfo) {
404+
final osInfo = switch (deviceInfo) {
405+
AndroidDeviceInfo(
406+
:var release) => 'Android $release', // "Android 14"
407+
IosDeviceInfo(
408+
:var systemVersion) => 'iOS $systemVersion', // "iOS 17.4"
409+
MacOsDeviceInfo(
410+
:var majorVersion,
411+
:var minorVersion,
412+
:var patchVersion) => 'macOS $majorVersion.$minorVersion.$patchVersion', // "macOS 14.5.0"
413+
WindowsDeviceInfo() => 'Windows', // "Windows"
414+
LinuxDeviceInfo(
415+
:var name,
416+
:var versionId) => 'Linux; $name${versionId != null ? ' $versionId' : ''}', // "Linux; Fedora Linux 40" or "Linux; Fedora Linux"
417+
_ => throw UnimplementedError(),
418+
};
419+
final PackageInfo(:version, :buildNumber) = packageInfo;
420+
421+
// Possible examples:
422+
// 'ZulipFlutter/0.0.15+15 (Android 14)'
423+
// 'ZulipFlutter/0.0.15+15 (iOS 17.4)'
424+
// 'ZulipFlutter/0.0.15+15 (macOS 14.5.0)'
425+
// 'ZulipFlutter/0.0.15+15 (Windows)'
426+
// 'ZulipFlutter/0.0.15+15 (Linux; Fedora Linux 40)'
397427
return {
398-
// TODO(#467) include platform, platform version, and app version
399-
'User-Agent': 'ZulipFlutter',
428+
'User-Agent': 'ZulipFlutter/$version+$buildNumber ($osInfo)',
400429
};
401430
}

test/api/core_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:http/http.dart' as http;
66
import 'package:test/scaffolding.dart';
77
import 'package:zulip/api/core.dart';
88
import 'package:zulip/api/exception.dart';
9+
import 'package:zulip/model/binding.dart';
910
import 'package:zulip/model/localizations.dart';
1011

1112
import '../model/binding.dart';
@@ -320,6 +321,41 @@ void main() {
320321
check(st.toString()).contains("distinctivelyNamedFromJson");
321322
}
322323
});
324+
325+
group('ApiConnection user-agent', () {
326+
Future<void> checkUserAgent(String expectedUserAgent) async {
327+
return FakeApiConnection.with_(account: eg.selfAccount,
328+
userAgentHeader: testBinding.userAgentHeader,
329+
(connection) async {
330+
connection.prepare(json: {});
331+
await connection.get(kExampleRouteName, (json) => json, 'example/route', null);
332+
check(connection.lastRequest!).isA<http.Request>()
333+
.headers.deepEquals({
334+
...authHeader(email: eg.selfAccount.email, apiKey: eg.selfAccount.apiKey),
335+
...{'User-Agent': expectedUserAgent},
336+
});
337+
});
338+
}
339+
340+
final packageInfo = PackageInfo(version: '0.0.1', buildNumber: '1');
341+
342+
final testCases = [
343+
('ZulipFlutter/0.0.1+1 (Android 14)', AndroidDeviceInfo(release: '14', sdkInt: 34), packageInfo),
344+
('ZulipFlutter/0.0.1+1 (iOS 17.4)', IosDeviceInfo(systemVersion: '17.4'), packageInfo),
345+
('ZulipFlutter/0.0.1+1 (macOS 14.5.0)', MacOsDeviceInfo(majorVersion: 14, minorVersion: 5, patchVersion: 0), packageInfo),
346+
('ZulipFlutter/0.0.1+1 (Windows)', WindowsDeviceInfo(), packageInfo),
347+
('ZulipFlutter/0.0.1+1 (Linux; Fedora Linux 40)', LinuxDeviceInfo(name: 'Fedora Linux', versionId: '40'), packageInfo),
348+
('ZulipFlutter/0.0.1+1 (Linux; Fedora Linux)', LinuxDeviceInfo(name: 'Fedora Linux', versionId: null), packageInfo),
349+
];
350+
351+
for (final (userAgent, deviceInfo, packageInfo) in testCases) {
352+
test('matches $userAgent', () async {
353+
testBinding.deviceInfoResult = deviceInfo;
354+
testBinding.packageInfoResult = packageInfo;
355+
await checkUserAgent(userAgent);
356+
});
357+
}
358+
});
323359
}
324360

325361
class DistinctiveError extends Error {

test/model/binding.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ class TestZulipBinding extends ZulipBinding {
294294

295295
@override
296296
Map<String, String> userAgentHeader() {
297-
return _userAgentHeader ??= buildUserAgentHeader();
297+
return _userAgentHeader ??= buildUserAgentHeader(maybeDeviceInfo!, maybePackageInfo!);
298298
}
299299
}
300300

0 commit comments

Comments
 (0)