@@ -1237,3 +1237,203 @@ def test_create_impression_event_without_cmab_uuid(self):
12371237 EventFactory .HTTP_VERB ,
12381238 EventFactory .HTTP_HEADERS ,
12391239 )
1240+
1241+
1242+ class EventFactoryIdNormalizationIntegrationTest (base .BaseTest ):
1243+ """FSSDK-12813: end-to-end decision-event ID normalization.
1244+
1245+ These tests build real ``ImpressionEvent`` instances using crafted
1246+ Experiment/Variation objects, then call ``EventFactory.create_log_event``
1247+ and inspect the dispatched payload. They exercise FR-001..FR-009.
1248+ """
1249+
1250+ def setUp (self , * args , ** kwargs ):
1251+ base .BaseTest .setUp (self , 'config_dict_with_multiple_experiments' )
1252+ self .logger = logger .NoOpLogger ()
1253+
1254+ def _build_impression (
1255+ self ,
1256+ experiment_id ,
1257+ layer_id ,
1258+ variation_id ,
1259+ rule_type = 'experiment' ,
1260+ ):
1261+ """Build an ImpressionEvent with the provided raw ID values.
1262+
1263+ ``experiment_id``/``layer_id``/``variation_id`` are inserted verbatim
1264+ so tests can exercise empty/non-string/non-numeric inputs.
1265+ """
1266+ from optimizely .entities import Experiment , Variation
1267+ from optimizely .event .user_event import EventContext , ImpressionEvent
1268+
1269+ experiment = Experiment (
1270+ id = experiment_id ,
1271+ key = 'exp_key' ,
1272+ status = 'Running' ,
1273+ audienceIds = [],
1274+ variations = [],
1275+ forcedVariations = {},
1276+ trafficAllocation = [],
1277+ layerId = layer_id ,
1278+ )
1279+ variation = Variation (
1280+ id = variation_id ,
1281+ key = 'variation_key' ,
1282+ featureEnabled = True ,
1283+ ) if isinstance (variation_id , str ) else None
1284+
1285+ event_context = EventContext (
1286+ account_id = '12001' ,
1287+ project_id = '111001' ,
1288+ revision = '42' ,
1289+ anonymize_ip = False ,
1290+ region = 'US' ,
1291+ )
1292+ return ImpressionEvent (
1293+ event_context = event_context ,
1294+ user_id = 'test_user' ,
1295+ experiment = experiment ,
1296+ visitor_attributes = [],
1297+ variation = variation ,
1298+ flag_key = 'flag_key' ,
1299+ rule_key = 'rule_key' ,
1300+ rule_type = rule_type ,
1301+ enabled = True ,
1302+ )
1303+
1304+ def _dispatched_decision (self , impression_event ):
1305+ """Return (decision_dict, event_dict) for an impression event."""
1306+ log_event = EventFactory .create_log_event (impression_event , self .logger )
1307+ snapshot = log_event .params ['visitors' ][0 ]['snapshots' ][0 ]
1308+ return snapshot ['decisions' ][0 ], snapshot ['events' ][0 ]
1309+
1310+ # ------------------------------------------------------------------ FR-001
1311+ def test_valid_campaign_id_is_passed_through (self ):
1312+ impression = self ._build_impression ('111127' , '111182' , '111129' )
1313+ decision , event = self ._dispatched_decision (impression )
1314+ self .assertEqual ('111182' , decision ['campaign_id' ])
1315+ # FR-009: entity_id mirrors campaign_id byte-for-byte.
1316+ self .assertEqual (decision ['campaign_id' ], event ['entity_id' ])
1317+
1318+ # ------------------------------------------------------------------ FR-002
1319+ def test_empty_campaign_id_falls_back_to_experiment_id (self ):
1320+ impression = self ._build_impression ('111127' , '' , '111129' )
1321+ decision , event = self ._dispatched_decision (impression )
1322+ self .assertEqual ('111127' , decision ['campaign_id' ])
1323+ self .assertEqual ('111127' , event ['entity_id' ])
1324+
1325+ def test_non_numeric_campaign_id_falls_back_to_experiment_id (self ):
1326+ impression = self ._build_impression ('111127' , 'campaign_a' , '111129' )
1327+ decision , event = self ._dispatched_decision (impression )
1328+ self .assertEqual ('111127' , decision ['campaign_id' ])
1329+ self .assertEqual ('111127' , event ['entity_id' ])
1330+
1331+ def test_whitespace_campaign_id_falls_back_to_experiment_id (self ):
1332+ impression = self ._build_impression ('111127' , ' ' , '111129' )
1333+ decision , event = self ._dispatched_decision (impression )
1334+ self .assertEqual ('111127' , decision ['campaign_id' ])
1335+ self .assertEqual ('111127' , event ['entity_id' ])
1336+
1337+ # ------------------------------------------------------------------ FR-003
1338+ def test_valid_variation_id_is_passed_through (self ):
1339+ impression = self ._build_impression ('111127' , '111182' , '111129' )
1340+ decision , _ = self ._dispatched_decision (impression )
1341+ self .assertEqual ('111129' , decision ['variation_id' ])
1342+
1343+ # ------------------------------------------------------------------ FR-004
1344+ def test_empty_variation_id_becomes_none (self ):
1345+ impression = self ._build_impression ('111127' , '111182' , '' )
1346+ decision , _ = self ._dispatched_decision (impression )
1347+ self .assertIsNone (decision ['variation_id' ])
1348+
1349+ def test_non_numeric_variation_id_becomes_none (self ):
1350+ impression = self ._build_impression ('111127' , '111182' , 'variation_a' )
1351+ decision , _ = self ._dispatched_decision (impression )
1352+ self .assertIsNone (decision ['variation_id' ])
1353+
1354+ def test_whitespace_variation_id_becomes_none (self ):
1355+ impression = self ._build_impression ('111127' , '111182' , ' ' )
1356+ decision , _ = self ._dispatched_decision (impression )
1357+ self .assertIsNone (decision ['variation_id' ])
1358+
1359+ # ------------------------------------------------------------------ FR-005
1360+ def test_normalization_applies_to_rollout_decisions (self ):
1361+ impression = self ._build_impression (
1362+ '111127' , 'bad_layer' , 'bad_var' , rule_type = 'rollout'
1363+ )
1364+ decision , event = self ._dispatched_decision (impression )
1365+ self .assertEqual ('111127' , decision ['campaign_id' ])
1366+ self .assertIsNone (decision ['variation_id' ])
1367+ self .assertEqual ('111127' , event ['entity_id' ])
1368+
1369+ def test_normalization_applies_to_feature_test_decisions (self ):
1370+ impression = self ._build_impression (
1371+ '111127' , '' , '' , rule_type = 'feature-test'
1372+ )
1373+ decision , event = self ._dispatched_decision (impression )
1374+ self .assertEqual ('111127' , decision ['campaign_id' ])
1375+ self .assertIsNone (decision ['variation_id' ])
1376+ self .assertEqual ('111127' , event ['entity_id' ])
1377+
1378+ def test_normalization_applies_to_holdout_decisions (self ):
1379+ impression = self ._build_impression (
1380+ '111127' , '' , '' , rule_type = 'holdout'
1381+ )
1382+ decision , event = self ._dispatched_decision (impression )
1383+ self .assertEqual ('111127' , decision ['campaign_id' ])
1384+ self .assertIsNone (decision ['variation_id' ])
1385+ self .assertEqual ('111127' , event ['entity_id' ])
1386+
1387+ # ------------------------------------------------------------------ FR-006
1388+ def test_event_still_dispatches_when_all_ids_invalid (self ):
1389+ """FR-006: never drop / fail dispatch."""
1390+ impression = self ._build_impression ('' , '' , '' )
1391+ log_event = EventFactory .create_log_event (impression , self .logger )
1392+ self .assertIsNotNone (log_event )
1393+ decision , event = self ._dispatched_decision (impression )
1394+ # campaign_id and entity_id end up as '' but the event still
1395+ # dispatches and the two fields remain byte-equivalent.
1396+ self .assertEqual ('' , decision ['campaign_id' ])
1397+ self .assertEqual ('' , event ['entity_id' ])
1398+ self .assertIsNone (decision ['variation_id' ])
1399+
1400+ # ------------------------------------------------------------------ FR-009
1401+ def test_entity_id_equals_campaign_id_byte_for_byte (self ):
1402+ """FR-009: ``events[].entity_id`` must equal ``decisions[].campaign_id``."""
1403+ for layer_id , exp_id , expected in [
1404+ ('111182' , '111127' , '111182' ), # campaign_id wins
1405+ ('' , '111127' , '111127' ), # falls back to experiment_id
1406+ ('bad' , '111127' , '111127' ), # fallback on invalid
1407+ ('007' , '111127' , '007' ), # leading zeros preserved
1408+ ]:
1409+ with self .subTest (layer_id = layer_id , exp_id = exp_id ):
1410+ impression = self ._build_impression (exp_id , layer_id , '111129' )
1411+ decision , event = self ._dispatched_decision (impression )
1412+ self .assertEqual (expected , decision ['campaign_id' ])
1413+ self .assertEqual (decision ['campaign_id' ], event ['entity_id' ])
1414+
1415+ # ----------------------------------------------------------------- FR-010
1416+ def test_conversion_event_entity_id_unchanged (self ):
1417+ """FR-010: conversion events derive entity_id from event.id, not the
1418+ normalizer.
1419+ """
1420+ from optimizely .event .user_event_factory import UserEventFactory
1421+
1422+ with mock .patch ('time.time' , return_value = 42.123 ), mock .patch (
1423+ 'uuid.uuid4' , return_value = 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c'
1424+ ):
1425+ conversion_event = UserEventFactory .create_conversion_event (
1426+ self .project_config ,
1427+ 'test_event' ,
1428+ 'test_user' ,
1429+ None ,
1430+ None ,
1431+ )
1432+ log_event = EventFactory .create_log_event (conversion_event , self .logger )
1433+ snapshot = log_event .params ['visitors' ][0 ]['snapshots' ][0 ]
1434+ # Conversion entity_id comes from the event.id of the conversion event
1435+ # and must NOT pass through the campaign_id normalizer.
1436+ self .assertEqual (
1437+ self .project_config .get_event ('test_event' ).id ,
1438+ snapshot ['events' ][0 ]['entity_id' ],
1439+ )
0 commit comments