@@ -1107,6 +1107,245 @@ describe("OptablePrebidAnalytics", () => {
11071107 } ) ;
11081108 } ) ;
11091109
1110+ describe ( "EID deduplication" , ( ) => {
1111+ beforeEach ( ( ) => {
1112+ analytics = new OptablePrebidAnalytics ( mockOptableInstance , {
1113+ tenant : "test-tenant" ,
1114+ } ) ;
1115+ } ) ;
1116+
1117+ it ( "should deduplicate EIDs by source when both ext.eids and eids contain same source" , async ( ) => {
1118+ const auctionEndEvent = {
1119+ auctionId : "auction-dedup" ,
1120+ bidderRequests : [
1121+ {
1122+ bidderCode : "bidder1" ,
1123+ bidderRequestId : "req-1" ,
1124+ ortb2 : {
1125+ site : { domain : "example.com" } ,
1126+ user : {
1127+ ext : {
1128+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "optable.co" } ] ,
1129+ } ,
1130+ eids : [ { inserter : "optable.co" , matcher : "matcher2" , source : "optable.co" } ] ,
1131+ } ,
1132+ } ,
1133+ bids : [ ] ,
1134+ } ,
1135+ ] ,
1136+ bidsReceived : [ ] ,
1137+ noBids : [ ] ,
1138+ timeoutBids : [ ] ,
1139+ } ;
1140+
1141+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1142+
1143+ // Should only have one source since they have the same source value
1144+ expect ( result . optableSources ) . toEqual ( [ "optable.co" ] ) ;
1145+ // The last occurrence (from eids, not ext.eids) should win due to deduplication
1146+ expect ( result . optableMatchers ) . toEqual ( [ "matcher2" ] ) ;
1147+ } ) ;
1148+
1149+ it ( "should preserve EIDs with different sources from ext.eids and eids" , async ( ) => {
1150+ const auctionEndEvent = {
1151+ auctionId : "auction-no-dedup" ,
1152+ bidderRequests : [
1153+ {
1154+ bidderCode : "bidder1" ,
1155+ bidderRequestId : "req-1" ,
1156+ ortb2 : {
1157+ site : { domain : "example.com" } ,
1158+ user : {
1159+ ext : {
1160+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "source1.optable.co" } ] ,
1161+ } ,
1162+ eids : [ { inserter : "optable.co" , matcher : "matcher2" , source : "source2.optable.co" } ] ,
1163+ } ,
1164+ } ,
1165+ bids : [ ] ,
1166+ } ,
1167+ ] ,
1168+ bidsReceived : [ ] ,
1169+ noBids : [ ] ,
1170+ timeoutBids : [ ] ,
1171+ } ;
1172+
1173+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1174+
1175+ // Should have both sources since they're different
1176+ expect ( result . optableSources ) . toHaveLength ( 2 ) ;
1177+ expect ( result . optableSources ) . toContain ( "source1.optable.co" ) ;
1178+ expect ( result . optableSources ) . toContain ( "source2.optable.co" ) ;
1179+ expect ( result . optableMatchers ) . toHaveLength ( 2 ) ;
1180+ expect ( result . optableMatchers ) . toContain ( "matcher1" ) ;
1181+ expect ( result . optableMatchers ) . toContain ( "matcher2" ) ;
1182+ } ) ;
1183+
1184+ it ( "should handle empty ext.eids and only use eids" , async ( ) => {
1185+ const auctionEndEvent = {
1186+ auctionId : "auction-only-eids" ,
1187+ bidderRequests : [
1188+ {
1189+ bidderCode : "bidder1" ,
1190+ bidderRequestId : "req-1" ,
1191+ ortb2 : {
1192+ site : { domain : "example.com" } ,
1193+ user : {
1194+ ext : {
1195+ eids : [ ] ,
1196+ } ,
1197+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "source1" } ] ,
1198+ } ,
1199+ } ,
1200+ bids : [ ] ,
1201+ } ,
1202+ ] ,
1203+ bidsReceived : [ ] ,
1204+ noBids : [ ] ,
1205+ timeoutBids : [ ] ,
1206+ } ;
1207+
1208+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1209+
1210+ expect ( result . optableSources ) . toEqual ( [ "source1" ] ) ;
1211+ expect ( result . optableMatchers ) . toEqual ( [ "matcher1" ] ) ;
1212+ } ) ;
1213+
1214+ it ( "should handle missing ext.eids and only use eids" , async ( ) => {
1215+ const auctionEndEvent = {
1216+ auctionId : "auction-no-ext-eids" ,
1217+ bidderRequests : [
1218+ {
1219+ bidderCode : "bidder1" ,
1220+ bidderRequestId : "req-1" ,
1221+ ortb2 : {
1222+ site : { domain : "example.com" } ,
1223+ user : {
1224+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "source1" } ] ,
1225+ } ,
1226+ } ,
1227+ bids : [ ] ,
1228+ } ,
1229+ ] ,
1230+ bidsReceived : [ ] ,
1231+ noBids : [ ] ,
1232+ timeoutBids : [ ] ,
1233+ } ;
1234+
1235+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1236+
1237+ expect ( result . optableSources ) . toEqual ( [ "source1" ] ) ;
1238+ expect ( result . optableMatchers ) . toEqual ( [ "matcher1" ] ) ;
1239+ } ) ;
1240+
1241+ it ( "should handle empty eids and only use ext.eids" , async ( ) => {
1242+ const auctionEndEvent = {
1243+ auctionId : "auction-only-ext-eids" ,
1244+ bidderRequests : [
1245+ {
1246+ bidderCode : "bidder1" ,
1247+ bidderRequestId : "req-1" ,
1248+ ortb2 : {
1249+ site : { domain : "example.com" } ,
1250+ user : {
1251+ ext : {
1252+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "source1" } ] ,
1253+ } ,
1254+ eids : [ ] ,
1255+ } ,
1256+ } ,
1257+ bids : [ ] ,
1258+ } ,
1259+ ] ,
1260+ bidsReceived : [ ] ,
1261+ noBids : [ ] ,
1262+ timeoutBids : [ ] ,
1263+ } ;
1264+
1265+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1266+
1267+ expect ( result . optableSources ) . toEqual ( [ "source1" ] ) ;
1268+ expect ( result . optableMatchers ) . toEqual ( [ "matcher1" ] ) ;
1269+ } ) ;
1270+
1271+ it ( "should deduplicate multiple EIDs with same source across different locations" , async ( ) => {
1272+ const auctionEndEvent = {
1273+ auctionId : "auction-multi-dedup" ,
1274+ bidderRequests : [
1275+ {
1276+ bidderCode : "bidder1" ,
1277+ bidderRequestId : "req-1" ,
1278+ ortb2 : {
1279+ site : { domain : "example.com" } ,
1280+ user : {
1281+ ext : {
1282+ eids : [
1283+ { inserter : "optable.co" , matcher : "matcher1" , source : "optable.co" } ,
1284+ { inserter : "other.co" , matcher : "other1" , source : "other.co" } ,
1285+ ] ,
1286+ } ,
1287+ eids : [
1288+ { inserter : "optable.co" , matcher : "matcher2" , source : "optable.co" } ,
1289+ { inserter : "other.co" , matcher : "other2" , source : "other.co" } ,
1290+ ] ,
1291+ } ,
1292+ } ,
1293+ bids : [ ] ,
1294+ } ,
1295+ ] ,
1296+ bidsReceived : [ ] ,
1297+ noBids : [ ] ,
1298+ timeoutBids : [ ] ,
1299+ } ;
1300+
1301+ const result = await analytics . toWitness ( auctionEndEvent , null ) ;
1302+
1303+ // Should only have one optable source and matcher due to deduplication
1304+ expect ( result . optableSources ) . toEqual ( [ "optable.co" ] ) ;
1305+ expect ( result . optableMatchers ) . toEqual ( [ "matcher2" ] ) ;
1306+ // Non-optable EIDs should be filtered out
1307+ expect ( result . optableMatchers ) . not . toContain ( "other1" ) ;
1308+ expect ( result . optableMatchers ) . not . toContain ( "other2" ) ;
1309+ } ) ;
1310+
1311+ it ( "should handle trackAuctionEnd with duplicate EIDs in ext.eids and eids" , async ( ) => {
1312+ const event = {
1313+ auctionId : "auction-track-dedup" ,
1314+ timeout : 3000 ,
1315+ bidderRequests : [
1316+ {
1317+ bidderCode : "bidder1" ,
1318+ bidderRequestId : "req-1" ,
1319+ ortb2 : {
1320+ site : { domain : "example.com" } ,
1321+ user : {
1322+ ext : {
1323+ eids : [ { inserter : "optable.co" , matcher : "matcher1" , source : "optable.co" } ] ,
1324+ } ,
1325+ eids : [ { inserter : "optable.co" , matcher : "matcher2" , source : "optable.co" } ] ,
1326+ } ,
1327+ } ,
1328+ bids : [ ] ,
1329+ } ,
1330+ ] ,
1331+ bidsReceived : [ ] ,
1332+ noBids : [ ] ,
1333+ timeoutBids : [ ] ,
1334+ } ;
1335+
1336+ await analytics . trackAuctionEnd ( event ) ;
1337+
1338+ const storedAuction = analytics [ "auctions" ] . get ( "auction-track-dedup" ) ;
1339+ expect ( storedAuction ) . toBeDefined ( ) ;
1340+
1341+ const payload = await analytics . toWitness ( storedAuction . auctionEnd , null ) ;
1342+ // Should only have one source after deduplication
1343+ expect ( payload . optableSources ) . toEqual ( [ "optable.co" ] ) ;
1344+ // The last matcher should win
1345+ expect ( payload . optableMatchers ) . toEqual ( [ "matcher2" ] ) ;
1346+ } ) ;
1347+ } ) ;
1348+
11101349 describe ( "trackAuctionEnd - edge cases" , ( ) => {
11111350 beforeEach ( ( ) => {
11121351 analytics = new OptablePrebidAnalytics ( mockOptableInstance ) ;
0 commit comments