11import { EvaluationEngine } from '@amplitude/experiment-core' ;
22
3+ import { BehavioralTargetingManager } from '../behavioral-targeting' ;
4+ import { areBehaviorsEqual } from '../behavioral-targeting/util' ;
35import { DefaultWebExperimentClient , INJECT_ACTION } from '../experiment' ;
46import {
57 ElementAppearedTriggerValue ,
@@ -55,11 +57,16 @@ export class SubscriptionManager {
5557 private webExperimentClient : DefaultWebExperimentClient ;
5658 private messageBus : MessageBus ;
5759 private pageObjects : PageObjects ;
60+ private readonly behavioralTargetingManager :
61+ | BehavioralTargetingManager
62+ | undefined ;
5863 private options : initOptions ;
5964 private readonly globalScope : typeof globalThis ;
6065 private pageChangeSubscribers : Set < ( event : PageChangeEvent ) => void > =
6166 new Set ( ) ;
6267 private lastNotifiedActivePages : PageObjects = { } ;
68+ private lastNotifiedActiveBehavioralFlags : Map < string , Set < string > > =
69+ new Map ( ) ;
6370 private intersectionObservers : Map < string , IntersectionObserver > = new Map ( ) ;
6471 private elementVisibilityState : Map < string , boolean > = new Map ( ) ;
6572 private elementAppearedState : Set < string > = new Set ( ) ;
@@ -99,12 +106,14 @@ export class SubscriptionManager {
99106 webExperimentClient : DefaultWebExperimentClient ,
100107 messageBus : MessageBus ,
101108 pageObjects : PageObjects ,
109+ behavioralTargetingManager : BehavioralTargetingManager | undefined ,
102110 options : initOptions ,
103111 globalScope : typeof globalThis ,
104112 ) {
105113 this . webExperimentClient = webExperimentClient ;
106114 this . messageBus = messageBus ;
107115 this . pageObjects = pageObjects ;
116+ this . behavioralTargetingManager = behavioralTargetingManager ;
108117 this . options = options ;
109118 this . globalScope = globalScope ;
110119 }
@@ -325,6 +334,12 @@ export class SubscriptionManager {
325334 url_change : new Set ( ) ,
326335 } ;
327336
337+ // Ensure analytics_event subscription is set up if behavioral targeting exists
338+ // This ensures behavioral targeting evaluation runs even without analytics_event page objects
339+ if ( this . behavioralTargetingManager ) {
340+ triggerTypeExperimentMap . analytics_event = new Set ( ) ;
341+ }
342+
328343 for ( const [ experiment , pages ] of Object . entries ( this . pageObjects ) ) {
329344 for ( const page of Object . values ( pages ) ) {
330345 if ( ! triggerTypeExperimentMap [ page . trigger_type ] ) {
@@ -361,6 +376,7 @@ export class SubscriptionManager {
361376 for ( const triggerType of Object . keys ( triggerTypeExperimentMap ) ) {
362377 this . messageBus . groupSubscribe ( triggerType as MessageType , ( payload ) => {
363378 const isUrlChange = triggerType === 'url_change' ;
379+ const isAnalyticsEvent = triggerType === 'analytics_event' ;
364380
365381 // Handle URL change: reset state and revert injections
366382 if ( isUrlChange ) {
@@ -379,19 +395,52 @@ export class SubscriptionManager {
379395 this . lastNotifiedActivePages ,
380396 ) ;
381397
398+ // Get current behavioral state and check if it changed
399+ const activeBehavioralFlags =
400+ this . webExperimentClient . behavioralTargetingManager ?. getMatchedBehaviors ( ) ;
401+
402+ const behaviorsChanged =
403+ isAnalyticsEvent &&
404+ ! areBehaviorsEqual (
405+ activeBehavioralFlags ,
406+ this . lastNotifiedActiveBehavioralFlags ,
407+ ) ;
408+ if ( behaviorsChanged ) {
409+ this . webExperimentClient . updateUserWithBehaviors ( ) ;
410+ }
382411 // Skip processing in visual editor mode or internal updates
383412 const isInternalUpdate =
384413 'updateActivePages' in payload && payload . updateActivePages ;
385414 const shouldApplyVariants =
386415 ! isInternalUpdate &&
387416 ! this . options . isVisualEditorMode &&
388- ( pagesChanged || isUrlChange ) ;
417+ ( pagesChanged || behaviorsChanged || isUrlChange ) ;
389418
390419 if ( shouldApplyVariants ) {
391420 // Determine which experiments to apply variants for
392- const relevantFlags = isUrlChange
393- ? undefined // All experiments
394- : Array . from ( triggerTypeExperimentMap [ triggerType ] || [ ] ) ;
421+ let relevantFlags : string [ ] | undefined ;
422+
423+ if ( isUrlChange ) {
424+ relevantFlags = undefined ; // All experiments
425+ } else {
426+ // Combine flags from both page triggers and behavioral changes
427+ const pageTriggerFlags = Array . from (
428+ triggerTypeExperimentMap [ triggerType ] || [ ] ,
429+ ) ;
430+ const behaviorFlags = behaviorsChanged
431+ ? Array . from (
432+ new Set ( [
433+ ...Array . from ( activeBehavioralFlags ?. keys ( ) || [ ] ) ,
434+ ...Array . from (
435+ this . lastNotifiedActiveBehavioralFlags ?. keys ( ) || [ ] ,
436+ ) ,
437+ ] ) ,
438+ )
439+ : [ ] ;
440+ relevantFlags = Array . from (
441+ new Set ( [ ...pageTriggerFlags , ...behaviorFlags ] ) ,
442+ ) ;
443+ }
395444
396445 // Apply non-preview variants
397446 this . webExperimentClient . applyVariants ( {
@@ -427,6 +476,16 @@ export class SubscriptionManager {
427476 if ( pagesChanged || isUrlChange ) {
428477 this . scheduleDebugNotification ( ) ;
429478 }
479+
480+ // Update last notified behaviors if they changed
481+ if ( behaviorsChanged && activeBehavioralFlags ) {
482+ this . lastNotifiedActiveBehavioralFlags = new Map (
483+ Array . from ( activeBehavioralFlags . entries ( ) ) . map ( ( [ key , value ] ) => [
484+ key ,
485+ new Set ( value ) ,
486+ ] ) ,
487+ ) ;
488+ }
430489 } ) ;
431490 }
432491 } ;
@@ -451,6 +510,7 @@ export class SubscriptionManager {
451510 this . maxScrollPercentage = 0 ;
452511 this . pageLoadTime = Date . now ( ) ;
453512 this . analyticsEventState . clear ( ) ;
513+ this . lastNotifiedActiveBehavioralFlags = new Map ( ) ;
454514
455515 // Clear pending click timeouts
456516 for ( const timeout of this . clickTimeouts . values ( ) ) {
0 commit comments