feat(auth): append fallback x-goog-api-client header and prepare v2.3.4 (#761)

* feat(auth): append fallback x-goog-api-client header

* refactor(auth): share x-goog-api-client constants and verify packageVersion against pubspec.yaml

* chore(auth): prepare googleapis_auth v2.3.4 release

* refactor(auth): unify x-goog-api-client injection into addXGoogApiClientHeader and privatize constants

* fix(auth): use _dartVersion getter to satisfy prefer_const_declarations

* refactor(auth): inline single-use constants in version.dart
diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md
index 1cf4d13..fe4c017 100644
--- a/googleapis_auth/CHANGELOG.md
+++ b/googleapis_auth/CHANGELOG.md
@@ -1,5 +1,8 @@
-## 2.3.4-wip
+## 2.3.4
 
+- Added a fallback `x-goog-api-client` tracking header to `AuthenticatedClient`,
+  `ApiKeyClient`, `signBlob`, and OAuth/STS token requests when not already set
+  on outgoing requests.
 - Updated README for the current Google Cloud Console credentials UI,
   Desktop app OAuth clients, and optional client secrets.
 
diff --git a/googleapis_auth/lib/src/auth_http_utils.dart b/googleapis_auth/lib/src/auth_http_utils.dart
index 551fe3d..4530de5 100644
--- a/googleapis_auth/lib/src/auth_http_utils.dart
+++ b/googleapis_auth/lib/src/auth_http_utils.dart
@@ -12,6 +12,7 @@
 import 'auth_functions.dart';
 import 'http_client_base.dart';
 import 'service_account_credentials.dart';
+import 'version.dart';
 
 /// Will close the underlying `http.Client` depending on a constructor argument.
 class AuthenticatedClient extends DelegatingClient implements AuthClient {
@@ -40,6 +41,9 @@
     if (quotaProject != null) {
       modifiedRequest.headers['X-Goog-User-Project'] = quotaProject!;
     }
+
+    addXGoogApiClientHeader(modifiedRequest.headers);
+
     final response = await baseClient.send(modifiedRequest);
     final wwwAuthenticate = response.headers['www-authenticate'];
     if (wwwAuthenticate != null) {
@@ -83,6 +87,7 @@
 
     final modifiedRequest = RequestImpl(request.method, url, request.finalize())
       ..headers.addAll(request.headers);
+    addXGoogApiClientHeader(modifiedRequest.headers);
     return baseClient.send(modifiedRequest);
   }
 }
diff --git a/googleapis_auth/lib/src/iam_signer.dart b/googleapis_auth/lib/src/iam_signer.dart
index d757f21..d674ca3 100644
--- a/googleapis_auth/lib/src/iam_signer.dart
+++ b/googleapis_auth/lib/src/iam_signer.dart
@@ -11,6 +11,7 @@
 import 'metadata_server_stub.dart'
     if (dart.library.io) 'metadata_server_io.dart';
 import 'utils.dart';
+import 'version.dart';
 
 /// Signs the given [data] using the IAM Credentials API.
 ///
@@ -68,7 +69,7 @@
 
   final response = await client.post(
     signBlobUrl,
-    headers: {'Content-Type': 'application/json'},
+    headers: addXGoogApiClientHeader({'Content-Type': 'application/json'}),
     body: requestBody,
   );
 
diff --git a/googleapis_auth/lib/src/utils.dart b/googleapis_auth/lib/src/utils.dart
index e0858d8..bccf3cd 100644
--- a/googleapis_auth/lib/src/utils.dart
+++ b/googleapis_auth/lib/src/utils.dart
@@ -12,6 +12,7 @@
 import 'access_token.dart';
 import 'auth_endpoints.dart';
 import 'exceptions.dart';
+import 'version.dart';
 
 /// Due to differences of clock speed, network latency, etc. we
 /// will shorten expiry dates by 20 seconds.
@@ -101,6 +102,7 @@
     if (headers != null) {
       request.headers.addAll(headers);
     }
+    addXGoogApiClientHeader(request.headers);
     switch (body) {
       case null:
         break;
diff --git a/googleapis_auth/lib/src/version.dart b/googleapis_auth/lib/src/version.dart
new file mode 100644
index 0000000..0fa5da0
--- /dev/null
+++ b/googleapis_auth/lib/src/version.dart
@@ -0,0 +1,20 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'version_fallback.dart' if (dart.library.io) 'version_io.dart';
+
+/// Must be kept in sync with `pubspec.yaml` (verified by `test/version_test.dart`).
+final _xGoogApiClientHeaderValue = 'gl-dart/$dartVersion auth/2.3.4';
+
+/// Adds the fallback `x-goog-api-client` header to [headers] if not already
+/// present, and returns [headers].
+Map<String, String> addXGoogApiClientHeader(Map<String, String> headers) {
+  const header = 'x-goog-api-client';
+  if (!headers.keys.any((k) => k.toLowerCase() == header)) {
+    headers[header] = _xGoogApiClientHeaderValue;
+  }
+  return headers;
+}
diff --git a/googleapis_auth/lib/src/version_fallback.dart b/googleapis_auth/lib/src/version_fallback.dart
new file mode 100644
index 0000000..f6a29e7
--- /dev/null
+++ b/googleapis_auth/lib/src/version_fallback.dart
@@ -0,0 +1,9 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+/// Fallback Dart version as `Platform` from `dart:io` is not available when
+/// targeting JavaScript/Wasm.
+String get dartVersion => 'unknown';
diff --git a/googleapis_auth/lib/src/version_io.dart b/googleapis_auth/lib/src/version_io.dart
new file mode 100644
index 0000000..6040f6b
--- /dev/null
+++ b/googleapis_auth/lib/src/version_io.dart
@@ -0,0 +1,10 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'dart:io' show Platform;
+
+/// Major.minor.patch version of the current Dart SDK.
+final dartVersion = Platform.version.split(RegExp('[^0-9]')).take(3).join('.');
diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml
index 9c428d8..d0b95a9 100644
--- a/googleapis_auth/pubspec.yaml
+++ b/googleapis_auth/pubspec.yaml
@@ -1,5 +1,5 @@
 name: googleapis_auth
-version: 2.3.4-wip
+version: 2.3.4
 description: Obtain Access credentials for Google services using OAuth 2.0
 repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth
 
diff --git a/googleapis_auth/test/http_client_base_test.dart b/googleapis_auth/test/http_client_base_test.dart
index 2429de1..4d3c3a6 100644
--- a/googleapis_auth/test/http_client_base_test.dart
+++ b/googleapis_auth/test/http_client_base_test.dart
@@ -90,6 +90,7 @@
     test('no-query-string adds key', () {
       final mock = mockClient((Request request) {
         expect('${request.url}', 'http://localhost/abc?$keyEncoded');
+        expectXGoogApiClientHeader(request);
         return responseF();
       });
 
diff --git a/googleapis_auth/test/iam_signer_test.dart b/googleapis_auth/test/iam_signer_test.dart
index b98249c..6917122 100644
--- a/googleapis_auth/test/iam_signer_test.dart
+++ b/googleapis_auth/test/iam_signer_test.dart
@@ -13,11 +13,14 @@
 import 'package:http/testing.dart';
 import 'package:test/test.dart';
 
+import 'test_utils.dart';
+
 void main() {
   test('signBlob posts to correct URL and returns signed blob', () async {
     final client = MockClient((request) async {
       if (request.url.path.contains('signBlob')) {
         expect(request.url.toString(), contains('test-email%40example.com'));
+        expectXGoogApiClientHeader(request);
         final body = jsonDecode(request.body) as Map<String, dynamic>;
         expect(body['payload'], isNotNull);
         return Response(
diff --git a/googleapis_auth/test/impersonated_auth_client_test.dart b/googleapis_auth/test/impersonated_auth_client_test.dart
index aa34c56..da32fd5 100644
--- a/googleapis_auth/test/impersonated_auth_client_test.dart
+++ b/googleapis_auth/test/impersonated_auth_client_test.dart
@@ -472,6 +472,7 @@
     final customBaseClient = mockClient((request) async {
       authenticatedRequestCalled = true;
       expect(request.headers['Authorization'], 'Bearer impersonated-token');
+      expectXGoogApiClientHeader(request);
       return http.Response('ok', 200);
     }, expectClose: false);
 
diff --git a/googleapis_auth/test/oauth2_test.dart b/googleapis_auth/test/oauth2_test.dart
index 5cd1289..9ec50f5 100644
--- a/googleapis_auth/test/oauth2_test.dart
+++ b/googleapis_auth/test/oauth2_test.dart
@@ -329,11 +329,12 @@
             expectAsync1((request) async {
               expect(request.method, 'POST');
               expect(request.url, url);
-              expect(request.headers, hasLength(1));
+              expect(request.headers, hasLength(2));
               expect(
                 request.headers,
                 containsPair('Authorization', 'Bearer bar'),
               );
+              expectXGoogApiClientHeader(request);
 
               return Response('', 204);
             }),
@@ -347,7 +348,8 @@
         expect(response.statusCode, 204);
       });
 
-      test('successful request with quotaProject', () async {
+      test('preserves existing x-goog-api-client header', () async {
+        const existingHeader = 'gl-dart/3.8.0 gdcl/17.0.0';
         final client = authenticatedClient(
           mockClient(
             expectAsync1((request) async {
@@ -360,8 +362,38 @@
               );
               expect(
                 request.headers,
+                containsPair('x-goog-api-client', existingHeader),
+              );
+
+              return Response('', 204);
+            }),
+            expectClose: false,
+          ),
+          credentials,
+        );
+
+        final req = RequestImpl('POST', url)
+          ..headers['X-Goog-Api-Client'] = existingHeader;
+        final response = await client.send(req);
+        expect(response.statusCode, 204);
+      });
+
+      test('successful request with quotaProject', () async {
+        final client = authenticatedClient(
+          mockClient(
+            expectAsync1((request) async {
+              expect(request.method, 'POST');
+              expect(request.url, url);
+              expect(request.headers, hasLength(3));
+              expect(
+                request.headers,
+                containsPair('Authorization', 'Bearer bar'),
+              );
+              expect(
+                request.headers,
                 containsPair('x-goog-user-project', 'test-quota-project'),
               );
+              expectXGoogApiClientHeader(request);
 
               return Response('', 204);
             }),
@@ -382,11 +414,12 @@
             expectAsync1((request) async {
               expect(request.method, 'POST');
               expect(request.url, url);
-              expect(request.headers, hasLength(1));
+              expect(request.headers, hasLength(2));
               expect(
                 request.headers,
                 containsPair('Authorization', 'Bearer bar'),
               );
+              expectXGoogApiClientHeader(request);
 
               const headers = {'www-authenticate': 'foobar'};
               return Response('', 401, headers: headers);
diff --git a/googleapis_auth/test/sts_auth_client_test.dart b/googleapis_auth/test/sts_auth_client_test.dart
index 8dfaa11..783c9f1 100644
--- a/googleapis_auth/test/sts_auth_client_test.dart
+++ b/googleapis_auth/test/sts_auth_client_test.dart
@@ -26,6 +26,7 @@
       scopes: ['s1'],
       baseClient: mockClient(expectClose: false, (Request request) async {
         if (request.url.toString() == 'https://sts.googleapis.com/v1/token') {
+          expectXGoogApiClientHeader(request);
           final body = jsonDecode(request.body) as Map<String, dynamic>;
           expect(body['subjectToken'], 'my-token');
           expect(body['audience'], 'my-audience');
diff --git a/googleapis_auth/test/test_utils.dart b/googleapis_auth/test/test_utils.dart
index 78af369..9f083b7 100644
--- a/googleapis_auth/test/test_utils.dart
+++ b/googleapis_auth/test/test_utils.dart
@@ -7,12 +7,23 @@
 import 'package:googleapis_auth/googleapis_auth.dart';
 import 'package:googleapis_auth/src/crypto/pem.dart';
 import 'package:googleapis_auth/src/utils.dart';
+import 'package:googleapis_auth/src/version.dart';
 import 'package:http/http.dart';
 import 'package:http/testing.dart';
 import 'package:test/test.dart';
 
 const jsonContentType = {'content-type': 'application/json'};
 
+void expectXGoogApiClientHeader(BaseRequest request) {
+  expect(
+    request.headers,
+    containsPair(
+      'x-goog-api-client',
+      addXGoogApiClientHeader({})['x-goog-api-client'],
+    ),
+  );
+}
+
 const isServerRequestFailedException =
     TypeMatcher<ServerRequestFailedException>();
 
diff --git a/googleapis_auth/test/version_test.dart b/googleapis_auth/test/version_test.dart
new file mode 100644
index 0000000..e017d40
--- /dev/null
+++ b/googleapis_auth/test/version_test.dart
@@ -0,0 +1,67 @@
+// Copyright 2026 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:googleapis_auth/src/version.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test(
+    'addXGoogApiClientHeader uses pubspec.yaml version and VM Dart version',
+    () async {
+      final pkgUri = await Isolate.resolvePackageUri(
+        Uri.parse('package:googleapis_auth/googleapis_auth.dart'),
+      );
+      expect(pkgUri, isNotNull, reason: 'package URI must resolve');
+
+      final pubspecFile = File.fromUri(pkgUri!.resolve('../pubspec.yaml'));
+      expect(
+        pubspecFile.existsSync(),
+        isTrue,
+        reason: 'pubspec.yaml must exist',
+      );
+
+      final content = pubspecFile.readAsStringSync();
+      final match = RegExp(
+        r'^version:\s*(\S+)',
+        multiLine: true,
+      ).firstMatch(content);
+      expect(
+        match,
+        isNotNull,
+        reason: 'version must be declared in pubspec.yaml',
+      );
+
+      final pubspecVersion = match!.group(1);
+      final expectedDartVersion = Platform.version
+          .split(RegExp('[^0-9]'))
+          .take(3)
+          .join('.');
+      expect(expectedDartVersion, matches(RegExp(r'^\d+\.\d+\.\d+$')));
+
+      final headers = addXGoogApiClientHeader({});
+      expect(headers, {
+        'x-goog-api-client':
+            'gl-dart/$expectedDartVersion auth/$pubspecVersion',
+      });
+    },
+  );
+
+  test(
+    'addXGoogApiClientHeader preserves existing header case-insensitively',
+    () {
+      final headers = addXGoogApiClientHeader({
+        'X-Goog-Api-Client': 'gl-dart/3.8.0 gdcl/17.0.0',
+      });
+      expect(headers, {'X-Goog-Api-Client': 'gl-dart/3.8.0 gdcl/17.0.0'});
+    },
+  );
+}