diff --git a/packages/firebase_admin_sdk/CHANGELOG.md b/packages/firebase_admin_sdk/CHANGELOG.md index 48d8af3e..3aeb2f75 100644 --- a/packages/firebase_admin_sdk/CHANGELOG.md +++ b/packages/firebase_admin_sdk/CHANGELOG.md @@ -1,5 +1,7 @@ ## 0.5.4-wip +- Fixed `Firestore` requests not carrying the SDK's usage-tracking headers + (`X-Firebase-Client`, `X-Goog-Api-Client`). - Update dependency `googleapis_auth: ^2.3.3` to fix `auth/insufficient-permission` errors with Application Default Credentials that have no quota project set. diff --git a/packages/firebase_admin_sdk/lib/src/app.dart b/packages/firebase_admin_sdk/lib/src/app.dart index 11c330fc..086e3dd6 100644 --- a/packages/firebase_admin_sdk/lib/src/app.dart +++ b/packages/firebase_admin_sdk/lib/src/app.dart @@ -35,7 +35,6 @@ import '../messaging.dart'; import '../security_rules.dart'; import '../storage.dart'; import 'utils/utils.dart'; -import 'version.g.dart'; part 'app/app_exception.dart'; part 'app/app_options.dart'; diff --git a/packages/firebase_admin_sdk/lib/src/app/firebase_user_agent_client.dart b/packages/firebase_admin_sdk/lib/src/app/firebase_user_agent_client.dart index 5b964f2e..c627e48b 100644 --- a/packages/firebase_admin_sdk/lib/src/app/firebase_user_agent_client.dart +++ b/packages/firebase_admin_sdk/lib/src/app/firebase_user_agent_client.dart @@ -32,9 +32,7 @@ class FirebaseUserAgentClient extends BaseClient @override Future send(BaseRequest request) { - request.headers['X-Firebase-Client'] = 'fire-admin-dart/$packageVersion'; - request.headers['X-Goog-Api-Client'] = - 'gl-dart/$dartVersion fire-admin/$packageVersion'; + request.headers.addAll(firebaseUserAgentHeaders); return _client.send(request); } diff --git a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart index f47b5c30..0df9a4ee 100644 --- a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart +++ b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart @@ -19,6 +19,7 @@ import 'package:google_cloud_firestore/google_cloud_firestore.dart' import 'package:meta/meta.dart'; import '../app.dart'; +import '../utils/utils.dart'; /// Default database ID used by Firestore const String kDefaultDatabaseId = '(default)'; @@ -152,6 +153,10 @@ class Firestore implements FirebaseService { settings = settings.copyWith(projectId: projectId); } + settings = settings.copyWith( + headers: {...firestoreUsageTrackingHeaders, ...?settings.headers}, + ); + return settings; } diff --git a/packages/firebase_admin_sdk/lib/src/utils/utils.dart b/packages/firebase_admin_sdk/lib/src/utils/utils.dart index 9ecf51a6..6a66c4a7 100644 --- a/packages/firebase_admin_sdk/lib/src/utils/utils.dart +++ b/packages/firebase_admin_sdk/lib/src/utils/utils.dart @@ -14,10 +14,42 @@ import 'dart:io'; +import '../version.g.dart'; + /// The current Dart SDK version in semver format (e.g. "3.3.0"). String get dartVersion => Platform.version.split(RegExp('[^0-9]')).take(3).join('.'); +/// The Firebase Admin SDK's `X-Firebase-Client` identity. +const String _fireAdminFirebaseClientId = 'fire-admin-dart/$packageVersion'; + +/// The Firebase Admin SDK's tag within `X-Goog-Api-Client`. +const String _fireAdminApiClientTag = 'fire-admin/$packageVersion'; + +/// Headers that identify a request as originating from this SDK, for +/// Firebase backend usage tracking. +/// +/// Used by `FirebaseUserAgentClient`, which wraps HTTP clients (Auth, +/// Messaging, and other services that go through `FirebaseApp.client`) that +/// don't already set a client-identification header of their own. +final Map firebaseUserAgentHeaders = Map.unmodifiable({ + 'X-Firebase-Client': _fireAdminFirebaseClientId, + 'X-Goog-Api-Client': 'gl-dart/$dartVersion $_fireAdminApiClientTag', +}); + +/// Headers to attach to Firestore requests to identify them as originating +/// from this SDK, for Firebase backend usage tracking. +/// +/// Unlike [firebaseUserAgentHeaders], `X-Goog-Api-Client` here omits the +/// `gl-dart/{version}` runtime tag: `google_cloud_firestore`'s +/// `FirestoreRequestClient` appends this value onto one that +/// `package:google_cloud_rpc` already set (which already carries that tag), +/// so repeating it here would duplicate it. +const Map firestoreUsageTrackingHeaders = { + 'X-Firebase-Client': _fireAdminFirebaseClientId, + 'X-Goog-Api-Client': _fireAdminApiClientTag, +}; + /// Generates the update mask for the provided object. /// Note this will ignore the last key with value `null`. List _generateUpdateMask(Object? obj, String root) { diff --git a/packages/firebase_admin_sdk/test/unit/firestore/firestore_test.dart b/packages/firebase_admin_sdk/test/unit/firestore/firestore_test.dart index 63de8ab7..c833f03e 100644 --- a/packages/firebase_admin_sdk/test/unit/firestore/firestore_test.dart +++ b/packages/firebase_admin_sdk/test/unit/firestore/firestore_test.dart @@ -16,6 +16,7 @@ import 'dart:io'; import 'package:firebase_admin_sdk/firebase_admin_sdk.dart'; import 'package:firebase_admin_sdk/src/firestore/firestore.dart'; +import 'package:firebase_admin_sdk/src/utils/utils.dart'; import 'package:google_cloud_firestore/google_cloud_firestore.dart' as gfs; import 'package:googleapis_auth/auth_io.dart' as auth; import 'package:mocktail/mocktail.dart'; @@ -254,6 +255,39 @@ void main() { }); }); + group('usage-tracking headers', () { + test('are attached when no user headers are provided', () { + final settings = firestoreService.buildSettingsForTesting('db', null); + + expect(settings.headers, firestoreUsageTrackingHeaders); + }); + + test('are merged with, not replaced by, user-provided headers', () { + final settings = firestoreService.buildSettingsForTesting( + 'db', + const gfs.Settings(headers: {'X-Trace-Id': 'abc123'}), + ); + + expect(settings.headers, { + ...firestoreUsageTrackingHeaders, + 'X-Trace-Id': 'abc123', + }); + }); + + test('can be overridden by a user-provided header of the same name', () { + final settings = firestoreService.buildSettingsForTesting( + 'db', + const gfs.Settings(headers: {'X-Firebase-Client': 'custom-value'}), + ); + + expect(settings.headers!['X-Firebase-Client'], 'custom-value'); + expect( + settings.headers!['X-Goog-Api-Client'], + firestoreUsageTrackingHeaders['X-Goog-Api-Client'], + ); + }); + }); + group('lifecycle', () { test('should terminate all databases on delete', () async { final db1 = firestoreService.getDatabase('lifecycle-1');