Events Reference
Experience SDK uses an event-driven architecture. All events are emitted via the SDK's event emitter and can have multiple listeners.
Why Events?
Unlike callback-based approaches, events provide:
- Multiple Consumers - Many listeners can react to one event
- Decoupled Architecture - Banner plugin doesn't know about tracking
- Serializable Config - No function references in config (JSON-safe)
- Testable - Easy to mock and verify
- Composable - Add/remove listeners dynamically
Core Events
experiences:evaluated
Emitted after evaluating all registered experiences.
When: After calling experiences.evaluate()
Payload:
{
decision: Decision; // The evaluation result
experience?: Experience; // The matched experience (if any)
}Example:
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show) {
console.log(`Show ${experience?.id}`);
console.log('Reasons:', decision.reasons);
} else {
console.log('Hide all experiences');
console.log('Reasons:', decision.reasons);
}
});Use Cases:
- Track impressions
- Custom rendering logic
- Debug logging
- A/B test tracking
experiences:action
Emitted when a user interacts with an experience (e.g., clicks a CTA button).
When: User clicks a button configured with button.action or button.url
Payload:
{
experienceId: string; // ID of the experience
action: string; // Action identifier
url?: string; // Navigation URL (if provided)
timestamp: number; // Event timestamp
}Example:
experiences.on('experiences:action', ({ experienceId, action, url }) => {
// Track click
analytics.track('Experience Action', { experienceId, action });
// Custom logic for specific actions
if (action === 'subscribe') {
showSubscriptionForm();
}
// URL navigation happens automatically if provided
console.log('Navigating to:', url);
});Use Cases:
- Track button clicks
- Custom navigation logic
- Trigger side effects (modals, forms, etc.)
- A/B test goal tracking
Note: If button.url is provided, the SDK automatically navigates after emitting the event.
experiences:dismissed
Emitted when a user dismisses an experience (e.g., closes a banner).
When: User clicks the dismiss/close button
Payload:
{
experienceId: string; // ID of the dismissed experience
timestamp: number; // Event timestamp
}Example:
experiences.on('experiences:dismissed', ({ experienceId }) => {
// Track dismissal
analytics.track('Experience Dismissed', { experienceId });
// Optionally suppress for longer period
suppressExperience(experienceId, '7d');
});Use Cases:
- Track dismissal rates
- Custom suppression logic
- User preference tracking
- Engagement metrics
Note: Dismissals do NOT count as impressions for frequency capping.
sdk:ready
Emitted when SDK initialization is complete.
When: After experiences.init() finishes
Payload: None
Example:
experiences.on('sdk:ready', () => {
console.log('SDK initialized');
// Safe to register experiences now
});sdk:destroy
Emitted before SDK is destroyed.
When: Before experiences.destroy() completes
Payload: None
Example:
experiences.on('sdk:destroy', () => {
console.log('Cleaning up...');
// Unsubscribe from external services
});Event Patterns
Multiple Listeners
Events can have multiple listeners (unlike callbacks):
// Listener 1: jstag3 tracking
experiences.on('experiences:action', ({ experienceId, action }) => {
window.jstag.send('experience_action', { experienceId, action });
});
// Listener 2: Google Analytics
experiences.on('experiences:action', ({ experienceId, action }) => {
gtag('event', 'experience_action', { experienceId, action });
});
// Listener 3: Custom business logic
experiences.on('experiences:action', ({ action }) => {
if (action === 'accept-cookies') {
setCookieConsent(true);
}
});Unsubscribing
Store the unsubscribe function to stop listening:
const unsubscribe = experiences.on('experiences:action', handler);
// Later...
unsubscribe(); // Stop listeningConditional Logic
Use event payloads to implement conditional logic:
experiences.on('experiences:action', ({ experienceId, action }) => {
// Only track certain experiences
if (experienceId.startsWith('promo-')) {
trackPromoClick(action);
}
// Different behavior per action
switch (action) {
case 'subscribe':
showSubscriptionModal();
break;
case 'accept-cookies':
setCookieConsent(true);
break;
case 'dismiss':
// Custom dismissal logic
break;
}
});Async Handlers
Event handlers can be async:
experiences.on('experiences:action', async ({ experienceId, action }) => {
// Make API call
const response = await fetch('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ experienceId, action, timestamp: Date.now() })
});
if (response.ok) {
console.log('Tracked successfully');
}
});Integration Examples
jstag3 (Lytics)
Track all experience events to Lytics:
// Track impressions
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
window.jstag.send('experience_shown', {
experience_id: experience.id,
experience_type: experience.type,
targeting: experience.targeting,
frequency: experience.frequency,
reasons: decision.reasons
});
}
});
// Track actions
experiences.on('experiences:action', ({ experienceId, action, url }) => {
window.jstag.send('experience_action', {
experience_id: experienceId,
action: action,
destination_url: url
});
});
// Track dismissals
experiences.on('experiences:dismissed', ({ experienceId }) => {
window.jstag.send('experience_dismissed', {
experience_id: experienceId
});
});You can also create a reusable jstag3 plugin:
// jstag3Plugin.ts
export function initJstag3Tracking(experiences) {
if (!window.jstag) {
console.warn('jstag not available');
return;
}
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
window.jstag.send('experience_shown', {
experience_id: experience.id,
experience_type: experience.type
});
}
});
experiences.on('experiences:action', ({ experienceId, action, url }) => {
window.jstag.send('experience_action', {
experience_id: experienceId,
action: action,
destination_url: url
});
});
experiences.on('experiences:dismissed', ({ experienceId }) => {
window.jstag.send('experience_dismissed', {
experience_id: experienceId
});
});
}
// Usage
initJstag3Tracking(experiences);Google Analytics 4
Track experience events to GA4:
// Track impressions
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
gtag('event', 'experience_impression', {
experience_id: experience.id,
experience_type: experience.type
});
}
});
// Track actions
experiences.on('experiences:action', ({ experienceId, action }) => {
gtag('event', 'experience_click', {
experience_id: experienceId,
action_type: action
});
});
// Track dismissals
experiences.on('experiences:dismissed', ({ experienceId }) => {
gtag('event', 'experience_dismissed', {
experience_id: experienceId
});
});Segment
Track experience events to Segment:
// Track impressions
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
analytics.track('Experience Shown', {
experienceId: experience.id,
experienceType: experience.type,
reasons: decision.reasons
});
}
});
// Track actions
experiences.on('experiences:action', ({ experienceId, action, url }) => {
analytics.track('Experience Action', {
experienceId,
action,
url
});
});
// Track dismissals
experiences.on('experiences:dismissed', ({ experienceId }) => {
analytics.track('Experience Dismissed', {
experienceId
});
});Custom API
Send events to your own API:
const trackEvent = async (eventName, payload) => {
await fetch('/api/analytics/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: eventName,
payload,
timestamp: Date.now()
})
});
};
// Track all events
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
trackEvent('experience_shown', {
experienceId: experience.id,
experienceType: experience.type
});
}
});
experiences.on('experiences:action', ({ experienceId, action, url }) => {
trackEvent('experience_action', { experienceId, action, url });
});
experiences.on('experiences:dismissed', ({ experienceId }) => {
trackEvent('experience_dismissed', { experienceId });
});Comparison: Callbacks vs. Events
Pathfora (Callbacks - Monolithic)
// ❌ Not serializable, single consumer, tightly coupled
{
okMessage: 'Shop Now',
confirmAction: {
name: 'shop_cta',
callback: function(event, payload) {
// MUST define all logic here
analytics.track('Shop CTA', payload);
jstag.send('shop_cta', payload);
window.location = '/products';
}
}
}Problems:
- Can't serialize config to JSON (has function)
- Can't save to CMS/database
- Tight coupling (widget executes callback)
- Single handler only
- Hard to test
Our SDK (Events - Modular)
// ✅ Config is pure data (serializable)
{
button: {
text: 'Shop Now',
url: '/products',
action: 'shop-cta'
}
}
// App code (decoupled)
experiences.on('experiences:action', ({ action, url }) => {
// Custom analytics
analytics.track('Banner Action', { action, url });
});
// jstag3 plugin (separate concern)
experiences.on('experiences:action', ({ action }) => {
jstag.send('experience_action', { action });
});
// URL navigation happens automaticallyBenefits:
- ✅ Config is pure data (save to CMS!)
- ✅ Multiple consumers
- ✅ Decoupled (banner doesn't know about tracking)
- ✅ Easy to test
- ✅ Composable
Best Practices
1. Keep Event Handlers Lightweight
// ✅ Good: Quick, non-blocking
experiences.on('experiences:action', ({ experienceId, action }) => {
gtag('event', 'experience_action', { experienceId, action });
});
// ❌ Avoid: Heavy computation or long-running tasks
experiences.on('experiences:action', async ({ experienceId }) => {
// This could block for seconds
const analytics = await heavyAnalyticsInit();
await analytics.track('event');
});2. Unsubscribe When Done
// Store unsubscribe function
const unsubscribe = experiences.on('experiences:action', handler);
// Clean up when component unmounts
useEffect(() => {
return () => {
unsubscribe();
};
}, []);3. Use TypeScript for Type Safety
import type { Decision, Experience } from '@prosdevlab/experience-sdk';
experiences.on('experiences:evaluated', ({ decision, experience }: {
decision: Decision;
experience?: Experience;
}) => {
// TypeScript will help you
if (experience) {
console.log(experience.id); // Autocomplete works!
}
});4. Create Reusable Integration Functions
// integrations/analytics.ts
export function setupAnalyticsTracking(experiences) {
experiences.on('experiences:evaluated', ({ decision, experience }) => {
if (decision.show && experience) {
analytics.track('Experience Shown', { experienceId: experience.id });
}
});
experiences.on('experiences:action', ({ experienceId, action }) => {
analytics.track('Experience Action', { experienceId, action });
});
}
// main.ts
import { setupAnalyticsTracking } from './integrations/analytics';
setupAnalyticsTracking(experiences);Next Steps
- Getting Started - Basic event examples
- Banner Demo - See events in action
- API Reference - Complete API docs