@amplitude/experiment-tag 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,73 @@
1
+ import { BehavioralTargetingRules } from '../types';
2
+ /**
3
+ * Manages all behavioral targeting functionality including event storage,
4
+ * session management, and rule evaluation.
5
+ */
6
+ export declare class BehavioralTargetingManager {
7
+ private readonly sessionManager;
8
+ private readonly eventStorage;
9
+ private evaluator;
10
+ private readonly rules;
11
+ private readonly matchedBehaviors;
12
+ private readonly eventToBehaviors;
13
+ constructor(apiKey: string, initialRules?: BehavioralTargetingRules);
14
+ /**
15
+ * Track an event for behavioral targeting evaluation.
16
+ * Automatically updates the active behavior state for affected flags.
17
+ * @param eventType The event type/name
18
+ * @param properties Event properties
19
+ */
20
+ trackEvent(eventType: string, properties: Record<string, unknown>): void;
21
+ /**
22
+ * Check if a flag has behavioral targeting rules.
23
+ * @param flagKey The flag key to check
24
+ * @returns true if the flag has behavioral targeting rules
25
+ */
26
+ hasRules(flagKey: string): boolean;
27
+ /**
28
+ * Get the currently matched behavioral targeting rules.
29
+ * @returns Map of flag keys to sets of matched behavior IDs
30
+ */
31
+ getMatchedBehaviors(): Map<string, Set<string>>;
32
+ /**
33
+ * Cleanup method to remove event listeners and flush pending writes.
34
+ * Should be called when BehavioralTargetingManager is no longer needed.
35
+ */
36
+ cleanup(): void;
37
+ /**
38
+ * Build a map of event types to the behavior IDs and flag keys that reference them.
39
+ * This allows efficient lookup of which specific behaviors need re-evaluation when an event is tracked.
40
+ */
41
+ private buildEventToBehaviorsMap;
42
+ /**
43
+ * Extract all event types referenced in behavioral targeting rules.
44
+ * @param rules The behavioral targeting rules to scan
45
+ * @returns Set of event types referenced in the rules
46
+ */
47
+ private extractEventTypes;
48
+ /**
49
+ * Evaluate all behavioral targeting rules.
50
+ * Directly updates the matchedBehaviors state for all flags.
51
+ */
52
+ private evaluateAll;
53
+ /**
54
+ * Evaluate behavioral targeting rules for a specific event type.
55
+ * Only re-evaluates the specific behavior IDs that reference this event type.
56
+ * Directly updates the matchedBehaviors state for affected flags.
57
+ * @param eventType The event type/name
58
+ */
59
+ private evaluateEvent;
60
+ /**
61
+ * Evaluate behavioral targeting rules for a specific flag.
62
+ * Directly updates the matchedBehaviors state with all matching behavior IDs.
63
+ * @param flagKey The flag key to evaluate
64
+ */
65
+ private evaluateFlag;
66
+ /**
67
+ * Evaluate a specific behavior ID for a flag.
68
+ * Updates only the specified behavior ID in the matchedBehaviors state.
69
+ * @param flagKey The flag key to evaluate
70
+ * @param behaviorId The specific behavior ID to evaluate
71
+ */
72
+ private evaluateBehaviorId;
73
+ }
@@ -0,0 +1,36 @@
1
+ import { EventStorageManager } from './event-storage';
2
+ import { BehavioralTargeting } from './types';
3
+ /**
4
+ * Evaluates behavioral targeting rules using stored events.
5
+ */
6
+ export declare class BehavioralTargetingEvaluator {
7
+ private eventStorage;
8
+ private evaluationEngine;
9
+ constructor(eventStorage: EventStorageManager);
10
+ /**
11
+ * Evaluates a behavioral targeting rule (DNF structure).
12
+ * Returns true if the user matches the targeting criteria.
13
+ */
14
+ evaluate(rules: BehavioralTargeting): boolean;
15
+ /**
16
+ * Evaluates an AND group of conditions.
17
+ */
18
+ private evaluateAndGroup;
19
+ /**
20
+ * Evaluates a single condition set (with optional negation).
21
+ */
22
+ private evaluateConditionSet;
23
+ /**
24
+ * Evaluates a single behavioral condition.
25
+ */
26
+ private evaluateCondition;
27
+ /**
28
+ * Evaluates property filters using EvaluationEngine.
29
+ * The backend has already translated property filters to EvaluationCondition format.
30
+ */
31
+ private evaluatePropertyFilters;
32
+ /**
33
+ * Evaluates count operators.
34
+ */
35
+ private evaluateCountOperator;
36
+ }
@@ -0,0 +1,92 @@
1
+ import { SessionManager } from './session-manager';
2
+ /**
3
+ * Represents a stored event record.
4
+ */
5
+ export interface EventRecord {
6
+ id: number;
7
+ event_type: string;
8
+ timestamp: number;
9
+ session_id: string;
10
+ properties: Record<string, unknown>;
11
+ }
12
+ /**
13
+ * Manages event storage with in-memory cache and debounced localStorage persistence.
14
+ * All reads are from memory for performance, writes are debounced to localStorage.
15
+ */
16
+ export declare class EventStorageManager {
17
+ private static readonly MAX_EVENTS;
18
+ private static readonly DEBOUNCE_MS;
19
+ private sessionManager;
20
+ private memoryCache;
21
+ private debouncedWriteTimeout;
22
+ private hasPendingWrites;
23
+ private persistedEvents?;
24
+ private storageKey;
25
+ constructor(apiKey: string, sessionManager: SessionManager, persistedEvents?: Set<string>);
26
+ /**
27
+ * Adds an event to in-memory cache with automatic session tracking.
28
+ * Triggers debounced write to localStorage.
29
+ * If persistedEvents is set, only events with matching event types will be stored.
30
+ */
31
+ addEvent(eventType: string, properties?: Record<string, unknown>, eventTime?: number): void;
32
+ /**
33
+ * Gets all events from in-memory cache (for testing/debugging).
34
+ */
35
+ getAllEvents(): EventRecord[];
36
+ /**
37
+ * Gets events filtered by event type from in-memory cache.
38
+ */
39
+ getEventsByType(eventType: string): EventRecord[];
40
+ /**
41
+ * Gets events in a time window from in-memory cache.
42
+ */
43
+ getEventsInTimeWindow(eventType: string, timeType: 'current_session' | 'rolling', timeValue: number, interval?: 'day' | 'hour'): EventRecord[];
44
+ /**
45
+ * Clears all events from memory and localStorage.
46
+ */
47
+ clearEvents(): void;
48
+ /**
49
+ * Manually flushes pending writes to localStorage.
50
+ * Useful for testing or when immediate persistence is required.
51
+ */
52
+ flush(): void;
53
+ /**
54
+ * Gets event count for an event type.
55
+ */
56
+ getEventCount(eventType: string): number;
57
+ /**
58
+ * Gets event count in a time window.
59
+ */
60
+ getEventCountInTimeWindow(eventType: string, timeType: 'current_session' | 'rolling', timeValue: number, interval?: 'day' | 'hour'): number;
61
+ /**
62
+ * Loads data from localStorage into memory on initialization.
63
+ */
64
+ private loadFromLocalStorage;
65
+ /**
66
+ * Schedules a debounced write to localStorage.
67
+ * Resets the timer on each call to batch multiple writes together.
68
+ */
69
+ private scheduleDebouncedWrite;
70
+ /**
71
+ * Immediately writes in-memory cache to localStorage.
72
+ */
73
+ private flushToLocalStorage;
74
+ /**
75
+ * Sets up event handlers to flush data before page unload or when tab is hidden.
76
+ * This prevents data loss on page close or backgrounding.
77
+ */
78
+ private setupFlushHandlers;
79
+ /**
80
+ * Handler for beforeunload event.
81
+ */
82
+ private handleBeforeUnload;
83
+ /**
84
+ * Handler for visibilitychange event.
85
+ */
86
+ private handleVisibilityChange;
87
+ /**
88
+ * Cleanup method to remove event listeners.
89
+ * Should be called when EventStorageManager is no longer needed.
90
+ */
91
+ cleanup(): void;
92
+ }
@@ -0,0 +1,2 @@
1
+ export { BehavioralTargetingManager } from './behavioral-targeting-manager';
2
+ export { BehavioralTargeting, BehavioralCondition, BehavioralConditionSet, } from './types';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Manages browser session ID using sessionStorage.
3
+ * Session ID is unique per browser tab and cleared when tab closes.
4
+ */
5
+ export declare class SessionManager {
6
+ private storageKey;
7
+ private sessionId?;
8
+ private sessionStartTime?;
9
+ constructor(apiKey: string);
10
+ /**
11
+ * Gets the current session ID, creating one if it doesn't exist.
12
+ * Uses sessionStorage (cleared when tab closes).
13
+ */
14
+ getOrCreateSessionId(): string;
15
+ /**
16
+ * Gets the session start time in milliseconds since epoch.
17
+ */
18
+ getSessionStartTime(): number | undefined;
19
+ /**
20
+ * Generates a unique session ID.
21
+ */
22
+ private generateSessionId;
23
+ /**
24
+ * Clears the current session (for testing).
25
+ */
26
+ clearSession(): void;
27
+ }
@@ -0,0 +1,29 @@
1
+ import { EvaluationCondition } from '@amplitude/experiment-core';
2
+ /**
3
+ * Behavioral targeting uses DNF (Disjunctive Normal Form): OR of ANDs
4
+ * Structure: [[ConditionSet, ConditionSet], [ConditionSet]]
5
+ * - Outer array: OR operator between groups
6
+ * - Inner array: AND operator within each group
7
+ * - Each condition can be negated individually
8
+ */
9
+ export type BehavioralTargeting = Array<Array<BehavioralConditionSet>>;
10
+ /**
11
+ * A condition set with optional negation.
12
+ */
13
+ export interface BehavioralConditionSet {
14
+ condition: BehavioralCondition;
15
+ negated?: boolean;
16
+ }
17
+ /**
18
+ * A behavioral condition
19
+ */
20
+ export interface BehavioralCondition {
21
+ type: 'event';
22
+ type_value: string;
23
+ operator: '>=' | '>' | '=' | '<' | '<=' | '!=' | 'is set' | 'is not set';
24
+ operator_value: number;
25
+ time_type: 'current_session' | 'rolling';
26
+ time_value: number;
27
+ interval?: 'day' | 'hour';
28
+ event_props?: EvaluationCondition[];
29
+ }
@@ -1,7 +1,7 @@
1
1
  import { Plugin } from '@amplitude/analytics-types';
2
2
  import { ExperimentClient, Variants } from '@amplitude/experiment-js-client';
3
3
  import { MutationController } from 'dom-mutator';
4
- import { PageChangeEvent } from './subscriptions';
4
+ import { PageChangeEvent } from './subscriptions/subscriptions';
5
5
  import { WebExperimentClient, WebExperimentConfig } from './types';
6
6
  import { ApplyVariantsOptions, PageObject, PageObjects, PreviewVariantsOptions, RevertVariantsOptions } from './types';
7
7
  import type { DebugState } from './types/debug';
@@ -0,0 +1,46 @@
1
+ import { DefaultWebExperimentClient } from '../experiment';
2
+ import { PageObjects } from '../types';
3
+ import { MessageBus } from './message-bus';
4
+ type initOptions = {
5
+ useDefaultNavigationHandler?: boolean;
6
+ isVisualEditorMode?: boolean;
7
+ };
8
+ export type PageChangeEvent = {
9
+ activePages: PageObjects;
10
+ };
11
+ export declare class SubscriptionManager {
12
+ private webExperimentClient;
13
+ private messageBus;
14
+ private pageObjects;
15
+ private options;
16
+ private readonly globalScope;
17
+ private triggerManagers;
18
+ private pageChangeSubscribers;
19
+ private lastNotifiedActivePages;
20
+ constructor(webExperimentClient: DefaultWebExperimentClient, messageBus: MessageBus, pageObjects: PageObjects, options: initOptions, globalScope: typeof globalThis);
21
+ setPageObjects: (pageObjects: PageObjects) => void;
22
+ markUrlAsPublished: (url: string) => void;
23
+ initSubscriptions: () => void;
24
+ /**
25
+ * Adds a subscriber to the page change event. Returns a function to unsubscribe.
26
+ */
27
+ addPageChangeSubscriber: (callback: (event: PageChangeEvent) => void) => (() => void);
28
+ /**
29
+ * Get debug snapshots from all trigger managers
30
+ */
31
+ getManagerSnapshots(): Record<string, any>;
32
+ /**
33
+ * Create trigger managers only for trigger types that have page objects.
34
+ * Special handling:
35
+ * - element_appeared tracks selectors from element_appeared, element_visible, and scrolled_to
36
+ * - url_change is always initialized with ALL page objects
37
+ */
38
+ private initializeTriggerManagers;
39
+ private initializeManager;
40
+ private groupPageObjectsByType;
41
+ private setupPageObjectSubscriptions;
42
+ private isPageObjectActive;
43
+ private resetAllTriggers;
44
+ private revertInjections;
45
+ }
46
+ export {};
@@ -1,7 +1,7 @@
1
- import { DefaultWebExperimentClient } from './experiment';
2
- import { MessageBus } from './subscriptions/message-bus';
3
- import { PageObjects } from './types';
4
- import type { DebugState, PageObjectDebugInfo } from './types/debug';
1
+ import { DefaultWebExperimentClient } from '../experiment';
2
+ import { PageObjects } from '../types';
3
+ import { DebugState, PageObjectDebugInfo } from '../types/debug';
4
+ import { MessageBus } from './message-bus';
5
5
  type initOptions = {
6
6
  useDefaultNavigationHandler?: boolean;
7
7
  isVisualEditorMode?: boolean;
@@ -37,6 +37,8 @@ export declare class SubscriptionManager {
37
37
  private lastPublishedUrl;
38
38
  private debugStateSubscribers;
39
39
  private debugDebounceTimer;
40
+ private get contentDocument();
41
+ private get contentWindow();
40
42
  constructor(webExperimentClient: DefaultWebExperimentClient, messageBus: MessageBus, pageObjects: PageObjects, options: initOptions, globalScope: typeof globalThis);
41
43
  setPageObjects: (pageObjects: PageObjects) => void;
42
44
  markUrlAsPublished: (url: string) => void;
@@ -0,0 +1,80 @@
1
+ import { MessageBus, MessagePayloads, MessageType } from '../subscriptions/message-bus';
2
+ import { PageObject } from '../types';
3
+ import { TriggerManagerOptions } from './index';
4
+ /**
5
+ * Base interface that all trigger managers must implement.
6
+ * Each trigger type (element_visible, user_interaction, etc.) has its own manager.
7
+ */
8
+ export interface TriggerManager<TPayload = any> {
9
+ /**
10
+ * Trigger type this manager handles
11
+ */
12
+ readonly triggerType: MessageType;
13
+ /**
14
+ * Initialize listeners, observers, and event handlers for this trigger type.
15
+ * Called once during setup.
16
+ */
17
+ initialize(): void;
18
+ /**
19
+ * Check if a specific page object is currently active based on trigger state.
20
+ * @param page - The page object to check
21
+ * @param payload - Optional message payload (for event-driven triggers)
22
+ */
23
+ isActive(page: PageObject, payload?: TPayload): boolean;
24
+ /**
25
+ * Reset trigger state (called on URL change).
26
+ * Should clear state but may keep listeners active if appropriate.
27
+ */
28
+ reset(): void;
29
+ /**
30
+ * Get debug snapshot of current state (optional, for debugging).
31
+ */
32
+ getSnapshot?(): Record<string, any>;
33
+ /**
34
+ * Mark a URL as published (optional, for url_change manager).
35
+ */
36
+ markUrlAsPublished?(url: string): void;
37
+ /**
38
+ * Trigger an initial check (optional, for element_appeared manager).
39
+ */
40
+ triggerInitialCheck?(): void;
41
+ }
42
+ /**
43
+ * Abstract base class providing common functionality for trigger managers.
44
+ * Reduces boilerplate in individual managers.
45
+ */
46
+ export declare abstract class BaseTriggerManager<TPayload = any> implements TriggerManager<TPayload> {
47
+ protected readonly pageObjects: PageObject[];
48
+ protected readonly messageBus: MessageBus;
49
+ protected readonly globalScope: typeof globalThis;
50
+ protected readonly options?: TriggerManagerOptions | undefined;
51
+ constructor(pageObjects: PageObject[], messageBus: MessageBus, globalScope: typeof globalThis, options?: TriggerManagerOptions | undefined);
52
+ abstract readonly triggerType: MessageType;
53
+ abstract initialize(): void;
54
+ abstract isActive(page: PageObject, payload?: TPayload): boolean;
55
+ abstract reset(): void;
56
+ /**
57
+ * Publish message to message bus
58
+ */
59
+ protected publish(payload?: MessagePayloads[MessageType]): void;
60
+ /**
61
+ * Helper to get trigger value with proper typing
62
+ */
63
+ protected getTriggerValue<T>(page: PageObject): T;
64
+ /**
65
+ * Helper to clear a timeout safely
66
+ */
67
+ protected clearTimeout(timeoutId: ReturnType<typeof setTimeout>): void;
68
+ /**
69
+ * Helper to clear multiple timeouts
70
+ */
71
+ protected clearTimeouts(timeouts: Iterable<ReturnType<typeof setTimeout>>): void;
72
+ /**
73
+ * Helper to disconnect an observer safely
74
+ */
75
+ protected disconnectObserver(observer: IntersectionObserver): void;
76
+ /**
77
+ * Helper to disconnect multiple observers
78
+ */
79
+ protected disconnectObservers(observers: Iterable<IntersectionObserver>): void;
80
+ }
@@ -0,0 +1,20 @@
1
+ import { MessageBus, MessageType } from '../subscriptions/message-bus';
2
+ import { PageObject } from '../types';
3
+ import { TriggerManager } from './base-trigger-manager';
4
+ export * from './base-trigger-manager';
5
+ /**
6
+ * Options that can be passed to trigger managers during initialization.
7
+ */
8
+ export interface TriggerManagerOptions {
9
+ useDefaultNavigationHandler?: boolean;
10
+ }
11
+ /**
12
+ * Trigger types that have dedicated managers.
13
+ * Note: element_appeared_internal is a message type but not a trigger type.
14
+ */
15
+ type TriggerType = Exclude<MessageType, 'element_appeared_internal'>;
16
+ /**
17
+ * Registry of trigger managers available in this version.
18
+ * Trigger managers will be added in subsequent PRs.
19
+ */
20
+ export declare const TRIGGER_MANAGER_REGISTRY: Partial<Record<TriggerType, new (pageObjects: PageObject[], messageBus: MessageBus, globalScope: typeof globalThis, options?: TriggerManagerOptions) => TriggerManager>>;
@@ -1,6 +1,7 @@
1
1
  import { EvaluationCondition } from '@amplitude/experiment-core';
2
2
  import { ExperimentConfig, ExperimentUser } from '@amplitude/experiment-js-client';
3
3
  import { ExperimentClient, Variants } from '@amplitude/experiment-js-client';
4
+ import { BehavioralTargeting } from './behavioral-targeting';
4
5
  import { MessageType } from './subscriptions/message-bus';
5
6
  import type { DebugState } from './types/debug';
6
7
  export type ApplyVariantsOptions = {
@@ -69,6 +70,15 @@ export type PageObjects = {
69
70
  [id: string]: PageObject;
70
71
  };
71
72
  };
73
+ /**
74
+ * Map of behavioral targeting rules per flag key.
75
+ * Each flag has ONE outer OR array for behavioral targeting.
76
+ */
77
+ export type BehavioralTargetingRules = {
78
+ [flagKey: string]: {
79
+ [id: string]: BehavioralTargeting;
80
+ };
81
+ };
72
82
  export interface WebExperimentConfig extends ExperimentConfig {
73
83
  /**
74
84
  * Determines whether the default implementation for handling navigation will be used
@@ -1,4 +1,21 @@
1
1
  export declare function isMobileModeActive(): boolean;
2
+ /**
3
+ * Returns the device iframe element when the customer page is rendered inside
4
+ * it (mobile viewport mode), or null when running in the top frame.
5
+ */
6
+ export declare function getDeviceIframe(): HTMLIFrameElement | null;
7
+ /**
8
+ * Returns the Document for the customer page. In mobile viewport mode the
9
+ * customer site lives inside a same-origin iframe; otherwise it is the
10
+ * top-level document.
11
+ */
12
+ export declare function getCustomerDocument(globalScope: typeof globalThis): Document;
13
+ /**
14
+ * Returns the Window for the customer page. In mobile viewport mode the
15
+ * customer site lives inside a same-origin iframe; otherwise it is the
16
+ * top-level window.
17
+ */
18
+ export declare const getCustomerWindow: (globalScope: typeof globalThis) => Window & typeof globalThis;
2
19
  /**
3
20
  * Replaces the page DOM with a shell container that loads the customer site
4
21
  * in a same-origin iframe. Deferred until the document is fully parsed so
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared utilities for working with MutationObserver.
3
+ */
4
+ /**
5
+ * Check if a mutation is relevant to a specific CSS selector.
6
+ * Returns true if the mutation affects elements matching the selector.
7
+ *
8
+ * @param mutationList - List of mutations to check
9
+ * @param selector - CSS selector to match against
10
+ * @returns true if any mutation is relevant to the selector
11
+ */
12
+ export declare function isMutationRelevantToSelector(mutationList: MutationRecord[], selector: string): boolean;
@@ -0,0 +1,44 @@
1
+ import { UserInteractionType } from '../../types';
2
+ /**
3
+ * Utilities for generating consistent state keys.
4
+ * All key generation is centralized here to ensure consistency across the codebase.
5
+ *
6
+ * Keys use null character (\0) as delimiter to avoid collisions with user input.
7
+ */
8
+ export declare class TriggerStateKeys {
9
+ private static readonly DELIMITER;
10
+ /**
11
+ * Create visibility key: "selector\0ratio"
12
+ * Used for element_visible triggers with IntersectionObserver
13
+ */
14
+ static visibility(selector: string, ratio: number): string;
15
+ /**
16
+ * Parse visibility key back to components
17
+ */
18
+ static parseVisibility(key: string): {
19
+ selector: string;
20
+ ratio: number;
21
+ };
22
+ /**
23
+ * Create user interaction key: "selector\0type\0threshold"
24
+ * Used for tracking click/hover/focus interactions
25
+ */
26
+ static userInteraction(selector: string, type: UserInteractionType, threshold: number): string;
27
+ /**
28
+ * Create scrolled-to key: "selector\0offset"
29
+ * Used for element-based scrolled_to triggers
30
+ */
31
+ static scrolledTo(selector: string, offset: number): string;
32
+ /**
33
+ * Parse scrolled-to key back to components
34
+ */
35
+ static parseScrolledTo(key: string): {
36
+ selector: string;
37
+ offset: number;
38
+ };
39
+ /**
40
+ * Create timeout key for threshold-based interactions
41
+ * Used internally for managing hover/focus timeouts
42
+ */
43
+ static timeout(selector: string, threshold: number): string;
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amplitude/experiment-tag",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Amplitude Experiment Javascript Snippet",
5
5
  "author": "Amplitude",
6
6
  "homepage": "https://github.com/amplitude/experiment-js-client",
@@ -30,7 +30,7 @@
30
30
  "@amplitude/analytics-core": "^2.21.0",
31
31
  "@amplitude/analytics-types": "^2.11.0",
32
32
  "@amplitude/experiment-core": "^0.12.0",
33
- "@amplitude/experiment-js-client": "^1.20.3",
33
+ "@amplitude/experiment-js-client": "^1.20.4",
34
34
  "dom-mutator": "git+ssh://git@github.com/amplitude/dom-mutator#0294e723c6a8f85d448bdfa59999591f2ae9e476",
35
35
  "rollup-plugin-license": "^3.6.0"
36
36
  },
@@ -42,5 +42,5 @@
42
42
  "files": [
43
43
  "dist"
44
44
  ],
45
- "gitHead": "3f9fcfdf64e686fbd12f975c98537cd2262cfef2"
45
+ "gitHead": "5ddeb989c0c9ef622e80605fecbe7ac52702d608"
46
46
  }