2323secondary goal of this test. Detailed testing of the instrumentation
2424output is the purview of the other tests in this directory."""
2525
26+
27+ import subprocess
28+ import json
29+ import yaml
30+ import google .auth
31+ import google .auth .credentials
2632import google .genai
2733from google .genai import types as genai_types
2834import os
@@ -45,22 +51,28 @@ def _should_redact_header(header_key):
4551 return True
4652 if header_key .startswith ('sec-goog' ):
4753 return True
54+ if header_key in ['server' , 'server-timing' ]:
55+ return True
4856 return False
4957
5058
5159def _redact_headers (headers ):
60+ to_redact = []
5261 for header_key in headers :
5362 if _should_redact_header (header_key .lower ()):
54- del headers [header_key ]
55-
63+ to_redact .append (header_key )
64+ for header_key in to_redact :
65+ headers [header_key ] = "<REDACTED>"
5666
5767def _before_record_request (request ):
58- _redact_headers (request .headers )
68+ if request .headers :
69+ _redact_headers (request .headers )
5970 return request
6071
6172
6273def _before_record_response (response ):
63- _redact_headers (response .headers )
74+ if hasattr (response , "headers" ) and response .headers :
75+ _redact_headers (response .headers )
6476 return response
6577
6678
@@ -89,13 +101,91 @@ def vcr_config():
89101 'key'
90102 ],
91103 'filter_headers' : [
104+ 'x-goog-api-key' ,
92105 'authorization' ,
106+ 'server' ,
107+ 'Server'
108+ 'Server-Timing' ,
109+ 'Date' ,
93110 ],
94111 'before_record_request' : _before_record_request ,
95112 'before_record_response' : _before_record_response ,
113+ 'ignore_hosts' : [
114+ 'oauth2.googleapis.com' ,
115+ 'iam.googleapis.com' ,
116+ ],
96117 }
97118
98119
120+ class _LiteralBlockScalar (str ):
121+ """Formats the string as a literal block scalar, preserving whitespace and
122+ without interpreting escape characters"""
123+
124+
125+ def _literal_block_scalar_presenter (dumper , data ):
126+ """Represents a scalar string as a literal block, via '|' syntax"""
127+ return dumper .represent_scalar ("tag:yaml.org,2002:str" , data , style = "|" )
128+
129+
130+ @pytest .fixture (scope = "module" , autouse = True )
131+ def setup_yaml_pretty_formattinmg ():
132+ yaml .add_representer (_LiteralBlockScalar , _literal_block_scalar_presenter )
133+
134+
135+ def _process_string_value (string_value ):
136+ """Pretty-prints JSON or returns long strings as a LiteralBlockScalar"""
137+ try :
138+ json_data = json .loads (string_value )
139+ return _LiteralBlockScalar (json .dumps (json_data , indent = 2 ))
140+ except (ValueError , TypeError ):
141+ if len (string_value ) > 80 :
142+ return _LiteralBlockScalar (string_value )
143+ return string_value
144+
145+
146+ def _convert_body_to_literal (data ):
147+ """Searches the data for body strings, attempting to pretty-print JSON"""
148+ if isinstance (data , dict ):
149+ for key , value in data .items ():
150+ # Handle response body case (e.g., response.body.string)
151+ if key == "body" and isinstance (value , dict ) and "string" in value :
152+ value ["string" ] = _process_string_value (value ["string" ])
153+
154+ # Handle request body case (e.g., request.body)
155+ elif key == "body" and isinstance (value , str ):
156+ data [key ] = _process_string_value (value )
157+
158+ else :
159+ _convert_body_to_literal (value )
160+
161+ elif isinstance (data , list ):
162+ for idx , choice in enumerate (data ):
163+ data [idx ] = _convert_body_to_literal (choice )
164+
165+ return data
166+
167+
168+ class _PrettyPrintJSONBody :
169+ """This makes request and response body recordings more readable."""
170+
171+ @staticmethod
172+ def serialize (cassette_dict ):
173+ cassette_dict = _convert_body_to_literal (cassette_dict )
174+ return yaml .dump (
175+ cassette_dict , default_flow_style = False , allow_unicode = True
176+ )
177+
178+ @staticmethod
179+ def deserialize (cassette_string ):
180+ return yaml .load (cassette_string , Loader = yaml .Loader )
181+
182+
183+ @pytest .fixture (scope = "module" , autouse = True )
184+ def setup_vcr (vcr ):
185+ vcr .register_serializer ("yaml" , _PrettyPrintJSONBody )
186+ return vcr
187+
188+
99189@pytest .fixture
100190def instrumentor ():
101191 return GoogleGenAiSdkInstrumentor ()
@@ -116,9 +206,10 @@ def otel_mocker():
116206 result .uninstall ()
117207
118208
119- @pytest .fixture (autouse = True , params = [True , False ])
209+ @pytest .fixture (autouse = True , params = ["logcontent" , "excludecontent" ])
120210def setup_content_recording (request ):
121- os .environ ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" ] = str (request .param )
211+ enabled = request .param == "logcontent"
212+ os .environ ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" ] = str (enabled )
122213
123214
124215@pytest .fixture
@@ -131,10 +222,29 @@ def in_replay_mode(vcr_record_mode):
131222 return vcr_record_mode == RecordMode .NONE
132223
133224
225+ def _try_get_project_from_gcloud ():
226+ try :
227+ gcloud_call_result = subprocess .run ("gcloud config get project" , shell = True , capture_output = True )
228+ except subprocess .CalledProcessError :
229+ return None
230+ gcloud_output = gcloud_call_result .stdout .decode ()
231+ return gcloud_output .strip ()
232+
233+
134234@pytest .fixture
135235def gcloud_project (in_replay_mode ):
136236 if in_replay_mode :
137237 return "test-project"
238+ project_envs = ["GCLOUD_PROJECT" , "GOOGLE_CLOUD_PROJECT" ]
239+ for project_env in project_envs :
240+ project_env_val = os .getenv (project_env )
241+ if project_env_val :
242+ return project_env_val
243+ from_gcloud = _try_get_project_from_gcloud ()
244+ if from_gcloud :
245+ os .environ ["GOOGLE_CLOUD_PROJECT" ] = from_gcloud
246+ os .environ ["GCLOUD_PROJECT" ] = from_gcloud
247+ return from_gcloud
138248 _ , from_creds = google .auth .default ()
139249 return from_creds
140250
@@ -151,7 +261,7 @@ def gcloud_credentials(in_replay_mode):
151261 if in_replay_mode :
152262 return FakeCredentials ()
153263 creds , _ = google .auth .default ()
154- return creds
264+ return google . auth . credentials . with_scopes_if_required ( creds , [ "https://www.googleapis.com/auth/cloud-platform" ])
155265
156266
157267@pytest .fixture (autouse = True )
@@ -165,12 +275,13 @@ def gcloud_api_key(in_replay_mode):
165275@pytest .fixture
166276def nonvertex_client_factory (gcloud_api_key ):
167277 def _factory ():
278+ print (f"Using API key: { gcloud_api_key } " )
168279 return google .genai .Client (api_key = gcloud_api_key )
169280 return _factory
170281
171282
172283@pytest .fixture
173- def vertex_client_factory (in_replay_mode ):
284+ def vertex_client_factory (in_replay_mode , gcloud_project , gcloud_location , gcloud_credentials ):
174285 def _factory ():
175286 return google .genai .Client (
176287 vertexai = True ,
@@ -180,21 +291,26 @@ def _factory():
180291 return _factory
181292
182293
183- @pytest .fixture (params = [True , False ])
184- def use_vertex (request ):
294+ @pytest .fixture (params = ["vertexaiapi" , "geminiapi" ])
295+ def genai_sdk_backend (request ):
185296 return request .param
186297
187298
299+ @pytest .fixture
300+ def use_vertex (genai_sdk_backend ):
301+ return genai_sdk_backend == "vertexaiapi"
302+
303+
188304@pytest .fixture
189305def client (vertex_client_factory , nonvertex_client_factory , use_vertex ):
190306 if use_vertex :
191307 return vertex_client_factory ()
192308 return nonvertex_client_factory ()
193309
194310
195- @pytest .fixture (params = [True , False ])
311+ @pytest .fixture (params = ["sync" , "async" ])
196312def is_async (request ):
197- return request .param
313+ return request .param == "async"
198314
199315
200316@pytest .fixture (params = ["gemini-1.0-flash" , "gemini-2.0-flash" ])
0 commit comments