From b0987519befe716b78371ddf84e562f49f80b024 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 23 Jul 2026 22:27:24 +0100 Subject: [PATCH 1/6] fix(admin): attach usage-tracking headers to Firestore requests --- packages/firebase_admin_sdk/CHANGELOG.md | 1 + .../lib/src/firestore/firestore.dart | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/firebase_admin_sdk/CHANGELOG.md b/packages/firebase_admin_sdk/CHANGELOG.md index 48d8af3e..922519f7 100644 --- a/packages/firebase_admin_sdk/CHANGELOG.md +++ b/packages/firebase_admin_sdk/CHANGELOG.md @@ -1,5 +1,6 @@ ## 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/firestore/firestore.dart b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart index f47b5c30..a833163f 100644 --- a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart +++ b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart @@ -19,6 +19,8 @@ import 'package:google_cloud_firestore/google_cloud_firestore.dart' import 'package:meta/meta.dart'; import '../app.dart'; +import '../utils/utils.dart'; +import '../version.g.dart'; /// Default database ID used by Firestore const String kDefaultDatabaseId = '(default)'; @@ -152,6 +154,15 @@ class Firestore implements FirebaseService { settings = settings.copyWith(projectId: projectId); } + if (settings.headers == null) { + settings = settings.copyWith( + headers: { + 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', + 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', + }, + ); + } + return settings; } From bc5fbbaf77b22bfcb3324ac4805c68d97b6b0fce Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 23 Jul 2026 22:39:21 +0100 Subject: [PATCH 2/6] refactor(admin): share usage-tracking header values between FirebaseUserAgentClient and Firestore --- packages/firebase_admin_sdk/lib/src/app.dart | 1 - .../lib/src/app/firebase_user_agent_client.dart | 4 +--- .../lib/src/firestore/firestore.dart | 8 +------- .../firebase_admin_sdk/lib/src/utils/utils.dart | 14 ++++++++++++++ 4 files changed, 16 insertions(+), 11 deletions(-) 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 a833163f..002a4f71 100644 --- a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart +++ b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart @@ -20,7 +20,6 @@ import 'package:meta/meta.dart'; import '../app.dart'; import '../utils/utils.dart'; -import '../version.g.dart'; /// Default database ID used by Firestore const String kDefaultDatabaseId = '(default)'; @@ -155,12 +154,7 @@ class Firestore implements FirebaseService { } if (settings.headers == null) { - settings = settings.copyWith( - headers: { - 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', - 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', - }, - ); + settings = settings.copyWith(headers: firebaseUserAgentHeaders); } 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..9c0fdf42 100644 --- a/packages/firebase_admin_sdk/lib/src/utils/utils.dart +++ b/packages/firebase_admin_sdk/lib/src/utils/utils.dart @@ -14,10 +14,24 @@ 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('.'); +/// Headers that identify a request as originating from this SDK, for +/// Firebase backend usage tracking. +/// +/// Shared by `FirebaseUserAgentClient` (used by Auth, Messaging, and other +/// services that go through `FirebaseApp.client`) and the Firestore service, +/// which builds its own HTTP client and can't be wrapped by +/// `FirebaseUserAgentClient` directly. +Map get firebaseUserAgentHeaders => { + 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', + 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', +}; + /// Generates the update mask for the provided object. /// Note this will ignore the last key with value `null`. List _generateUpdateMask(Object? obj, String root) { From a1f161f39a62fbb6fe0ba2ab89e879b7f0035159 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 23 Jul 2026 23:28:31 +0100 Subject: [PATCH 3/6] fix(admin): avoid duplicate gl-dart tag when tagging Firestore requests --- .../lib/src/firestore/firestore.dart | 2 +- .../lib/src/utils/utils.dart | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart index 002a4f71..c8d4f9e0 100644 --- a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart +++ b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart @@ -154,7 +154,7 @@ class Firestore implements FirebaseService { } if (settings.headers == null) { - settings = settings.copyWith(headers: firebaseUserAgentHeaders); + settings = settings.copyWith(headers: firestoreUsageTrackingHeaders); } 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 9c0fdf42..68678642 100644 --- a/packages/firebase_admin_sdk/lib/src/utils/utils.dart +++ b/packages/firebase_admin_sdk/lib/src/utils/utils.dart @@ -23,15 +23,27 @@ String get dartVersion => /// Headers that identify a request as originating from this SDK, for /// Firebase backend usage tracking. /// -/// Shared by `FirebaseUserAgentClient` (used by Auth, Messaging, and other -/// services that go through `FirebaseApp.client`) and the Firestore service, -/// which builds its own HTTP client and can't be wrapped by -/// `FirebaseUserAgentClient` directly. +/// 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. Map get firebaseUserAgentHeaders => { 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', }; +/// 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. +Map get firestoreUsageTrackingHeaders => { + 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', + 'X-Goog-Api-Client': 'fire-admin/$packageVersion', +}; + /// Generates the update mask for the provided object. /// Note this will ignore the last key with value `null`. List _generateUpdateMask(Object? obj, String root) { From efdcebc3f679622f56192364bdfafa5cf707afbf Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 23 Jul 2026 23:44:36 +0100 Subject: [PATCH 4/6] fix(admin): address review feedback, merge rather than replace user headers --- .../lib/src/firestore/firestore.dart | 6 ++-- .../lib/src/utils/utils.dart | 6 ++-- .../test/unit/firestore/firestore_test.dart | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart index c8d4f9e0..0df9a4ee 100644 --- a/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart +++ b/packages/firebase_admin_sdk/lib/src/firestore/firestore.dart @@ -153,9 +153,9 @@ class Firestore implements FirebaseService { settings = settings.copyWith(projectId: projectId); } - if (settings.headers == null) { - settings = settings.copyWith(headers: firestoreUsageTrackingHeaders); - } + 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 68678642..3fab29e5 100644 --- a/packages/firebase_admin_sdk/lib/src/utils/utils.dart +++ b/packages/firebase_admin_sdk/lib/src/utils/utils.dart @@ -26,10 +26,10 @@ String get dartVersion => /// 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. -Map get firebaseUserAgentHeaders => { +final Map firebaseUserAgentHeaders = Map.unmodifiable({ 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', -}; +}); /// Headers to attach to Firestore requests to identify them as originating /// from this SDK, for Firebase backend usage tracking. @@ -39,7 +39,7 @@ Map get firebaseUserAgentHeaders => { /// `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. -Map get firestoreUsageTrackingHeaders => { +const Map firestoreUsageTrackingHeaders = { 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', 'X-Goog-Api-Client': 'fire-admin/$packageVersion', }; 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'); From 568696a2e8f370839bbee2686dc32b6d1752db9f Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 27 Jul 2026 12:30:49 +0100 Subject: [PATCH 5/6] docs(admin): wrap CHANGELOG entry to match surrounding style --- packages/firebase_admin_sdk/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/firebase_admin_sdk/CHANGELOG.md b/packages/firebase_admin_sdk/CHANGELOG.md index 922519f7..3aeb2f75 100644 --- a/packages/firebase_admin_sdk/CHANGELOG.md +++ b/packages/firebase_admin_sdk/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.5.4-wip -- Fixed `Firestore` requests not carrying the SDK's usage-tracking headers (`X-Firebase-Client`, `X-Goog-Api-Client`). +- 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. From 7583452bcc35f298435b90e336f1f2d514ca918f Mon Sep 17 00:00:00 2001 From: demolaf Date: Mon, 27 Jul 2026 12:50:27 +0100 Subject: [PATCH 6/6] refactor(admin): extract shared identity strings to avoid duplicating them across the two header maps --- .../firebase_admin_sdk/lib/src/utils/utils.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/firebase_admin_sdk/lib/src/utils/utils.dart b/packages/firebase_admin_sdk/lib/src/utils/utils.dart index 3fab29e5..6a66c4a7 100644 --- a/packages/firebase_admin_sdk/lib/src/utils/utils.dart +++ b/packages/firebase_admin_sdk/lib/src/utils/utils.dart @@ -20,6 +20,12 @@ import '../version.g.dart'; 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. /// @@ -27,8 +33,8 @@ String get dartVersion => /// 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': 'fire-admin-dart/$packageVersion', - 'X-Goog-Api-Client': 'gl-dart/$dartVersion fire-admin/$packageVersion', + 'X-Firebase-Client': _fireAdminFirebaseClientId, + 'X-Goog-Api-Client': 'gl-dart/$dartVersion $_fireAdminApiClientTag', }); /// Headers to attach to Firestore requests to identify them as originating @@ -40,8 +46,8 @@ final Map firebaseUserAgentHeaders = Map.unmodifiable({ /// `package:google_cloud_rpc` already set (which already carries that tag), /// so repeating it here would duplicate it. const Map firestoreUsageTrackingHeaders = { - 'X-Firebase-Client': 'fire-admin-dart/$packageVersion', - 'X-Goog-Api-Client': 'fire-admin/$packageVersion', + 'X-Firebase-Client': _fireAdminFirebaseClientId, + 'X-Goog-Api-Client': _fireAdminApiClientTag, }; /// Generates the update mask for the provided object.