2828
2929app = Flask (__name__ )
3030
31+ # The harness has one enable_compression flag but a test per codec, so each
32+ # adapter instance takes its codec from COMPRESSION and advertises only that
33+ # codec's encoding_<codec> capability. The SDK has no brotli support.
34+ COMPRESSION_CODECS = {
35+ "gzip" : CaptureCompression .GZIP ,
36+ "deflate" : CaptureCompression .DEFLATE ,
37+ "zstd" : CaptureCompression .ZSTD ,
38+ }
39+
40+
41+ def compression_name () -> str :
42+ return os .environ .get ("COMPRESSION" ) or "gzip"
43+
3144
3245class RequestInfo :
3346 """Information about an HTTP request made by the SDK"""
@@ -220,11 +233,11 @@ def patched_post_v1(
220233@app .route ("/health" , methods = ["GET" ])
221234def health ():
222235 """Health check endpoint"""
223- # No AI capture capability: `capture_ai` posts capture v1 to
224- # /i/v1/ai/events, which this harness version has no suite for.
225236 capabilities = [
226237 "capture_v1" ,
227- "encoding_gzip" ,
238+ "capture_ai_v1" ,
239+ "event_options" ,
240+ "encoding_" + compression_name (),
228241 "feature_flags_local_evaluation_v1" ,
229242 ]
230243 return jsonify (
@@ -267,18 +280,20 @@ def init():
267280 # Convert flush_interval from ms to seconds
268281 flush_interval = flush_interval_ms / 1000.0
269282
283+ compression = (
284+ COMPRESSION_CODECS [compression_name ()]
285+ if enable_compression
286+ else CaptureCompression .NONE
287+ )
270288 # Explicit reloads exercise the real loader without background polling
271289 # racing the harness's per-test definition snapshots.
272290 client_options = {
273291 "project_api_key" : api_key ,
274292 "host" : host ,
275293 "flush_at" : flush_at ,
276294 "flush_interval" : flush_interval ,
277- "capture_compression" : (
278- CaptureCompression .GZIP
279- if enable_compression
280- else CaptureCompression .NONE
281- ),
295+ "capture_compression" : compression ,
296+ "capture_ai_compression" : compression ,
282297 "max_retries" : max_retries ,
283298 "debug" : False ,
284299 "disable_geoip" : disable_geoip ,
@@ -308,56 +323,55 @@ def init():
308323 return jsonify ({"error" : str (e )}), 500
309324
310325
311- @app .route ("/capture" , methods = ["POST" ])
312- def capture ():
313- """Capture a single event"""
314- try :
315- if not state .client :
316- return jsonify ({"error" : "SDK not initialized" }), 400
326+ def _capture_with (capture_method_name : str ):
327+ """Capture one event through the named SDK method.
317328
318- data = request .json or {}
329+ Options pass through unchanged, and a supplied uuid stays on the wire. The
330+ response carries the uuid the SDK actually sent, which differs from the
331+ supplied one when the SDK canonicalizes or replaces it.
332+ """
333+ if not state .client :
334+ return jsonify ({"error" : "SDK not initialized" }), 400
319335
320- distinct_id = data .get ("distinct_id" )
321- event = data .get ("event" )
322- properties = data .get ("properties" )
323- timestamp = data .get ("timestamp" )
324- options = data .get ("options" )
336+ data = request .json or {}
325337
326- if not distinct_id :
327- return jsonify ({"error" : "distinct_id is required" }), 400
328- if not event :
329- return jsonify ({"error" : "event is required" }), 400
330-
331- # Fold capture-v1 options back into the magic `$`-prefixed properties the
332- # SDK lifts onto the wire `options` object. Renamed keys mirror the SDK's
333- # sentinel table; unknown keys get a bare `$` prefix.
334- if options :
335- properties = dict (properties or {})
336- option_to_property = {
337- "cookieless_mode" : "$cookieless_mode" ,
338- "disable_skew_correction" : "$ignore_sent_at" ,
339- "process_person_profile" : "$process_person_profile" ,
340- "product_tour_id" : "$product_tour_id" ,
341- }
342- for key , value in options .items ():
343- properties [option_to_property .get (key , "$" + key )] = value
338+ distinct_id = data .get ("distinct_id" )
339+ event = data .get ("event" )
340+ timestamp = data .get ("timestamp" )
341+
342+ if not distinct_id :
343+ return jsonify ({"error" : "distinct_id is required" }), 400
344+ if not event :
345+ return jsonify ({"error" : "event is required" }), 400
346+
347+ kwargs = {
348+ "distinct_id" : distinct_id ,
349+ "properties" : data .get ("properties" ),
350+ "options" : data .get ("options" ),
351+ }
352+ if timestamp :
353+ # Parse ISO8601 timestamp
354+ from dateutil .parser import parse
344355
345- # Capture event
346- kwargs = {"distinct_id" : distinct_id , "properties" : properties }
347- if timestamp :
348- # Parse ISO8601 timestamp
349- from dateutil .parser import parse
356+ kwargs ["timestamp" ] = parse (timestamp )
357+ if data .get ("uuid" ):
358+ kwargs ["uuid" ] = data ["uuid" ]
350359
351- kwargs [ "timestamp" ] = parse ( timestamp )
360+ uuid = getattr ( state . client , capture_method_name )( event , ** kwargs )
352361
353- uuid = state .client .capture (event , ** kwargs )
362+ # Track that we captured an event
363+ state .increment_captured ()
354364
355- # Track that we captured an event
356- state .increment_captured ()
365+ logger .info (f"Captured event: { event } for { distinct_id } , uuid={ uuid } " )
357366
358- logger . info ( f"Captured event: { event } for { distinct_id } , uuid= { uuid } " )
367+ return jsonify ({ "success" : True , " uuid" : uuid })
359368
360- return jsonify ({"success" : True , "uuid" : uuid })
369+
370+ @app .route ("/capture" , methods = ["POST" ])
371+ def capture ():
372+ """Capture a single event"""
373+ try :
374+ return _capture_with ("capture" )
361375 except Exception as e :
362376 logger .exception ("Error capturing event" )
363377 state .record_error (str (e ))
@@ -368,50 +382,7 @@ def capture():
368382def capture_ai ():
369383 """Capture a single AI event on the dedicated AI capture endpoint"""
370384 try :
371- if not state .client :
372- return jsonify ({"error" : "SDK not initialized" }), 400
373-
374- data = request .json or {}
375-
376- distinct_id = data .get ("distinct_id" )
377- event = data .get ("event" )
378- properties = data .get ("properties" )
379- timestamp = data .get ("timestamp" )
380- options = data .get ("options" )
381- supplied_uuid = data .get ("uuid" )
382-
383- if not distinct_id :
384- return jsonify ({"error" : "distinct_id is required" }), 400
385- if not event :
386- return jsonify ({"error" : "event is required" }), 400
387-
388- if options :
389- properties = dict (properties or {})
390- option_to_property = {
391- "cookieless_mode" : "$cookieless_mode" ,
392- "disable_skew_correction" : "$ignore_sent_at" ,
393- "process_person_profile" : "$process_person_profile" ,
394- "product_tour_id" : "$product_tour_id" ,
395- }
396- for key , value in options .items ():
397- properties [option_to_property .get (key , "$" + key )] = value
398-
399- kwargs = {"distinct_id" : distinct_id , "properties" : properties }
400- if timestamp :
401- from dateutil .parser import parse
402-
403- kwargs ["timestamp" ] = parse (timestamp )
404- # Unlike /capture, forward a supplied uuid so it's echoed back to the caller.
405- if supplied_uuid :
406- kwargs ["uuid" ] = supplied_uuid
407-
408- uuid = state .client .capture_ai (event , ** kwargs )
409-
410- state .increment_captured ()
411-
412- logger .info (f"Captured AI event: { event } for { distinct_id } , uuid={ uuid } " )
413-
414- return jsonify ({"success" : True , "uuid" : uuid })
385+ return _capture_with ("capture_ai" )
415386 except Exception as e :
416387 logger .exception ("Error capturing AI event" )
417388 state .record_error (str (e ))
@@ -638,7 +609,14 @@ def reset():
638609def main ():
639610 """Main entry point"""
640611 port = int (os .environ .get ("PORT" , 8080 ))
641- logger .info (f"Starting SDK Test Adapter on port { port } " )
612+ if compression_name () not in COMPRESSION_CODECS :
613+ raise SystemExit (
614+ f"unsupported COMPRESSION { compression_name ()!r} : "
615+ f"want one of { ', ' .join (COMPRESSION_CODECS )} "
616+ )
617+ logger .info (
618+ f"Starting SDK Test Adapter on port { port } (compression={ compression_name ()} )"
619+ )
642620 app .run (host = "0.0.0.0" , port = port , debug = False )
643621
644622
0 commit comments