Overview
Custom event tracking in FullStory allows you to capture specific user actions and behaviors that matter to your business. Unlike automatic session recording which captures everything, custom events let you mark important moments - like purchases, feature usage, errors, or conversions - making them easily searchable and analyzable.
Why track custom events?
- Quickly find sessions where specific actions occurred
- Build funnels and measure conversion rates
- Trigger surveys or notifications based on events
- Segment users by behaviors and actions
- Analyze feature adoption and usage patterns
- Debug issues by filtering sessions with error events
What you'll learn:
- How to implement
FS.event()tracking - Best practices for event naming and properties
- Framework-specific integration patterns
- Complete validation and testing procedures
- Troubleshooting common event tracking issues
Prerequisites
Before implementing event tracking:
- FullStory installed and recording sessions
- Access to your codebase or tag manager
- Event taxonomy defined (covered below)
- Understanding of key user journeys to track
- User identification strategy planned
Event Tracking API Reference
Basic Event Tracking
Track an event without properties:
FS.event('Button Clicked');
Event with Properties
Track an event with additional context:
FS.event('Product Purchased', {
product_id: 'SKU-12345',
product_name: 'Premium Widget',
price: 49.99,
category: 'Electronics',
quantity: 2
});
Event Property Types
FullStory supports several property types:
FS.event('Form Submitted', {
// Strings
form_name_str: 'contact_form',
// Numbers (integers and reals)
completion_time_real: 45.5,
fields_completed_int: 8,
// Booleans
newsletter_opt_in_bool: true,
// Dates
submitted_at_date: new Date(),
// Lists (arrays)
tags_strs: ['urgent', 'sales', 'enterprise']
});
Important: Property names must include type suffixes (_str, _int, _real, _bool, _date, _strs).
Configuration
Step 1: Define Event Taxonomy
Before writing code, establish naming conventions and categories.
Naming Convention
Recommended format: [Object] [Action] or [Action] [Object]
Examples:
// Good - Clear and descriptive
FS.event('Button Clicked');
FS.event('Purchase Completed');
FS.event('Video Played');
FS.event('Form Submitted');
// Bad - Vague or inconsistent
FS.event('click');
FS.event('done');
FS.event('vid');
Event Categories
Organize events into logical categories:
User Authentication:
FS.event('User Signed Up', {
method_str: 'google', // google, email, sso
plan_str: 'free', // free, premium, enterprise
source_str: 'organic' // organic, paid, referral
});
FS.event('User Logged In', {
method_str: 'password', // password, google, sso
remember_me_bool: true
});
FS.event('User Logged Out');
FS.event('Password Reset Requested', {
method_str: 'email'
});
E-Commerce Events:
FS.event('Product Viewed', {
product_id_str: 'SKU-12345',
product_name_str: 'Widget Pro',
price_real: 49.99,
category_str: 'Electronics'
});
FS.event('Product Added to Cart', {
product_id_str: 'SKU-12345',
quantity_int: 2,
price_real: 49.99
});
FS.event('Checkout Started', {
cart_value_real: 149.99,
item_count_int: 3
});
FS.event('Purchase Completed', {
order_id_str: 'ORD-789',
revenue_real: 149.99,
tax_real: 12.00,
shipping_real: 5.99,
item_count_int: 3,
payment_method_str: 'credit_card'
});
Product Usage Events:
FS.event('Feature Enabled', {
feature_name_str: 'Advanced Analytics',
plan_required_str: 'premium'
});
FS.event('Export Downloaded', {
export_type_str: 'CSV',
rows_int: 5000,
file_size_kb_int: 234
});
FS.event('Report Generated', {
report_type_str: 'Monthly Sales',
date_range_str: '2024-01-01 to 2024-01-31',
generation_time_ms_int: 2340
});
Engagement Events:
FS.event('Video Played', {
video_id_str: 'tutorial_123',
video_title_str: 'Getting Started Guide',
duration_seconds_int: 180,
autoplay_bool: false
});
FS.event('File Downloaded', {
file_name_str: 'pricing-guide.pdf',
file_type_str: 'pdf',
file_size_mb_real: 2.4
});
FS.event('Article Shared', {
article_id_str: 'blog-post-456',
share_method_str: 'twitter', // twitter, facebook, email, link
article_title_str: 'Best Practices for Analytics'
});
Error and Friction Events:
FS.event('Form Validation Error', {
form_name_str: 'checkout',
field_name_str: 'email',
error_type_str: 'invalid_format'
});
FS.event('API Request Failed', {
endpoint_str: '/api/checkout',
status_code_int: 500,
error_message_str: 'Internal server error',
retry_count_int: 2
});
FS.event('Payment Declined', {
payment_method_str: 'credit_card',
decline_reason_str: 'insufficient_funds',
amount_real: 149.99
});
FS.event('Session Timed Out', {
idle_time_seconds_int: 1800,
page_str: '/checkout'
});
Step 2: Create Implementation Plan
Document which events to implement in each phase:
Phase 1: Critical Business Events (Week 1)
- Purchase Completed
- Subscription Upgraded
- Free Trial Started
- Account Created
Phase 2: User Journey Events (Week 2)
- Onboarding Step Completed
- Feature First Used
- Settings Changed
- Profile Updated
Phase 3: Engagement & Analytics (Week 3)
Phase 4: Error & Support Events (Week 4)
- Error Occurred
- Support Ticket Created
- Feedback Submitted
- FAQ Viewed
Code Examples
Basic Implementation
Button Click Tracking
// Simple button click
document.getElementById('signup-btn').addEventListener('click', function() {
FS.event('Signup Button Clicked', {
location_str: 'homepage',
button_text_str: this.textContent,
campaign_str: 'summer_promo'
});
});
// Track all CTA buttons
document.querySelectorAll('[data-track-cta]').forEach(button => {
button.addEventListener('click', function() {
FS.event('CTA Button Clicked', {
button_id_str: this.id,
button_text_str: this.textContent,
page_str: window.location.pathname
});
});
});
Form Tracking
const form = document.getElementById('contact-form');
// Track form start (first field interaction)
let formStarted = false;
form.addEventListener('focus', function(e) {
if (!formStarted && e.target.matches('input, textarea, select')) {
formStarted = true;
FS.event('Form Started', {
form_name_str: 'contact_form',
page_str: window.location.pathname
});
}
}, true);
// Track form submission
form.addEventListener('submit', function(e) {
const formData = new FormData(form);
FS.event('Form Submitted', {
form_name_str: 'contact_form',
field_count_int: formData.keys().length,
newsletter_opt_in_bool: formData.get('newsletter') === 'yes'
});
});
// Track form abandonment
window.addEventListener('beforeunload', function() {
if (formStarted && !form.dataset.submitted) {
FS.event('Form Abandoned', {
form_name_str: 'contact_form',
page_str: window.location.pathname
});
}
});
form.addEventListener('submit', function() {
form.dataset.submitted = 'true';
});
E-Commerce Tracking
// Add to cart
function addToCart(product) {
// Your add-to-cart logic
cart.add(product);
// Track event
FS.event('Product Added to Cart', {
product_id_str: product.id,
product_name_str: product.name,
price_real: parseFloat(product.price),
quantity_int: 1,
category_str: product.category,
cart_total_real: cart.getTotal()
});
}
// Remove from cart
function removeFromCart(product) {
cart.remove(product);
FS.event('Product Removed from Cart', {
product_id_str: product.id,
cart_total_real: cart.getTotal(),
remaining_items_int: cart.items.length
});
}
// Begin checkout
function initiateCheckout() {
FS.event('Checkout Started', {
cart_value_real: cart.getTotal(),
item_count_int: cart.items.length,
has_coupon_bool: cart.hasCoupon(),
is_returning_customer_bool: user.isReturning
});
window.location.href = '/checkout';
}
// Complete purchase
function completePurchase(order) {
FS.event('Purchase Completed', {
order_id_str: order.id,
revenue_real: order.total,
tax_real: order.tax,
shipping_real: order.shipping,
discount_real: order.discount,
item_count_int: order.items.length,
payment_method_str: order.paymentMethod,
shipping_method_str: order.shippingMethod,
currency_str: 'USD'
});
}
Video Tracking
const video = document.getElementById('product-demo-video');
// Track video play
video.addEventListener('play', function() {
FS.event('Video Played', {
video_id_str: 'product_demo_2024',
video_title_str: video.getAttribute('data-title'),
duration_seconds_int: Math.floor(video.duration),
autoplay_bool: video.autoplay
});
});
// Track video milestones
let milestones = [25, 50, 75, 100];
let trackedMilestones = new Set();
video.addEventListener('timeupdate', function() {
const percentWatched = (video.currentTime / video.duration) * 100;
milestones.forEach(milestone => {
if (percentWatched >= milestone && !trackedMilestones.has(milestone)) {
trackedMilestones.add(milestone);
FS.event('Video Progress', {
video_id_str: 'product_demo_2024',
percent_watched_int: milestone,
current_time_seconds_int: Math.floor(video.currentTime)
});
}
});
});
// Track video completion
video.addEventListener('ended', function() {
FS.event('Video Completed', {
video_id_str: 'product_demo_2024',
total_time_seconds_int: Math.floor(video.duration)
});
});
Error Tracking
// Track JavaScript errors
window.addEventListener('error', function(e) {
FS.event('JavaScript Error', {
error_message_str: e.message,
error_file_str: e.filename,
error_line_int: e.lineno,
error_column_int: e.colno,
page_str: window.location.pathname
});
});
// Track unhandled promise rejections
window.addEventListener('unhandledrejection', function(e) {
FS.event('Unhandled Promise Rejection', {
reason_str: e.reason?.toString() || 'Unknown',
page_str: window.location.pathname
});
});
// Track API errors
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
FS.event('API Request Failed', {
endpoint_str: url,
status_code_int: response.status,
status_text_str: response.statusText
});
}
return await response.json();
} catch (error) {
FS.event('Network Error', {
endpoint_str: url,
error_message_str: error.message
});
throw error;
}
}
// Track form validation errors
function validateForm(form) {
const errors = [];
// Your validation logic
if (!isValidEmail(form.email.value)) {
errors.push({ field: 'email', error: 'invalid_format' });
}
if (errors.length > 0) {
FS.event('Form Validation Error', {
form_name_str: form.id,
error_count_int: errors.length,
fields_with_errors_strs: errors.map(e => e.field),
first_error_str: errors[0].error
});
}
return errors.length === 0;
}
Framework-Specific Implementation
React Implementation
Create a FullStory hook:
// hooks/useFullStory.js
import { useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
// Track page views in SPAs
export function usePageTracking() {
const location = useLocation();
useEffect(() => {
if (window.FS) {
FS.event('Page Viewed', {
path_str: location.pathname,
search_str: location.search,
page_title_str: document.title
});
}
}, [location]);
}
// Event tracking hook
export function useEventTracking() {
return useCallback((eventName, properties = {}) => {
if (window.FS) {
FS.event(eventName, properties);
} else {
console.warn('[FullStory] FS not loaded, event not tracked:', eventName);
}
}, []);
}
// User identification hook
export function useUserIdentification(user) {
useEffect(() => {
if (user && window.FS) {
FS.identify(user.id, {
email_str: user.email,
displayName_str: user.name,
plan_str: user.subscription?.plan,
accountType_str: user.accountType,
signupDate_date: new Date(user.signupDate)
});
}
}, [user]);
}
Usage in components:
import { usePageTracking, useEventTracking, useUserIdentification } from './hooks/useFullStory';
import { useAuth } from './hooks/useAuth';
function App() {
const { user } = useAuth();
// Track page views on route change
usePageTracking();
// Identify user when logged in
useUserIdentification(user);
return (
<div className="App">
{/* Your app content */}
</div>
);
}
function ProductPage({ product }) {
const trackEvent = useEventTracking();
const handleAddToCart = () => {
trackEvent('Product Added to Cart', {
product_id_str: product.id,
product_name_str: product.name,
price_real: product.price,
category_str: product.category
});
// Your add-to-cart logic
addProductToCart(product);
};
return (
<div>
<h1>{product.name}</h1>
<button to Cart</button>
</div>
);
}
Vue.js Implementation
Create a FullStory plugin:
// plugins/fullstory.js
export default {
install(app) {
// Event tracking method
app.config.globalProperties.$trackEvent = (eventName, properties = {}) => {
if (window.FS) {
window.FS.event(eventName, properties);
} else {
console.warn('[FullStory] FS not loaded:', eventName);
}
};
// User identification method
app.config.globalProperties.$identifyUser = (userId, userVars = {}) => {
if (window.FS) {
window.FS.identify(userId, userVars);
}
};
}
};
Register the plugin:
// main.js
import { createApp } from 'vue';
import FullStoryPlugin from './plugins/fullstory';
import App from './App.vue';
const app = createApp(App);
app.use(FullStoryPlugin);
app.mount('#app');
Track route changes:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [/* your routes */]
});
router.afterEach((to, from) => {
if (window.FS) {
window.FS.event('Page Viewed', {
path_str: to.path,
page_title_str: to.meta?.title || document.title,
from_page_str: from.path
});
}
});
export default router;
Usage in components:
export default {
name: 'ProductPage',
props: ['product'],
methods: {
addToCart() {
this.$trackEvent('Product Added to Cart', {
product_id_str: this.product.id,
product_name_str: this.product.name,
price_real: this.product.price
});
// Your add-to-cart logic
},
handleUserLogin(user) {
this.$identifyUser(user.id, {
email_str: user.email,
plan_str: user.plan
});
}
}
};
Angular Implementation
Create a FullStory service:
// services/fullstory.service.ts
import { Injectable } from '@angular/core';
declare global {
interface Window {
FS: any;
}
}
@Injectable({
providedIn: 'root'
})
export class FullStoryService {
trackEvent(eventName: string, properties?: Record<string, any>): void {
if (window.FS) {
window.FS.event(eventName, properties);
} else {
console.warn('[FullStory] FS not loaded:', eventName);
}
}
identify(userId: string, userVars?: Record<string, any>): void {
if (window.FS) {
window.FS.identify(userId, userVars);
}
}
getCurrentSessionURL(): string | null {
if (window.FS) {
return window.FS.getCurrentSessionURL();
}
return null;
}
shutdown(): void {
if (window.FS) {
window.FS.shutdown();
}
}
restart(): void {
if (window.FS) {
window.FS.restart();
}
}
}
Track navigation:
// app.component.ts
import { Component } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { FullStoryService } from './services/fullstory.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(
private router: Router,
private fullStory: FullStoryService
) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe((event: NavigationEnd) => {
this.fullStory.trackEvent('Page Viewed', {
path_str: event.urlAfterRedirects,
page_title_str: document.title
});
});
}
}
Usage in components:
import { Component } from '@angular/core';
import { FullStoryService } from './services/fullstory.service';
@Component({
selector: 'app-product',
template: `
<div>
<h1>{{ product.name }}</h1>
<button (click)="addToCart()">Add to Cart</button>
</div>
`
})
export class ProductComponent {
product: any;
constructor(private fullStory: FullStoryService) {}
addToCart(): void {
this.fullStory.trackEvent('Product Added to Cart', {
product_id_str: this.product.id,
product_name_str: this.product.name,
price_real: this.product.price
});
// Your add-to-cart logic
}
}
Google Tag Manager Implementation
For teams using GTM, you can trigger FullStory events via the data layer:
Push to data layer:
// When user clicks button
dataLayer.push({
'event': 'button_click',
'buttonName': 'Sign Up CTA',
'buttonLocation': 'homepage',
'campaign': 'summer_promo'
});
Create GTM tag:
- Go to Tags > New
- Tag Type: Custom HTML
- HTML:
<script>
if (window.FS) {
FS.event('Button Clicked', {
button_name_str: {{DL - Button Name}},
location_str: {{DL - Button Location}},
campaign_str: {{DL - Campaign}}
});
}
</script>
- Trigger: Custom Event
button_click - Save and publish
Validation Steps
Pre-Production Testing
Console Testing
// Enable FullStory debug mode (in console)
localStorage.setItem('fs_debug', 'true');
// Trigger your event
FS.event('Test Event', {
property_str: 'test value'
});
// Check console for confirmation
// You should see: "FullStory event: Test Event"
Verify Event Fire
// Create a wrapper to log events
const originalEvent = window.FS.event;
window.FS.event = function(eventName, properties) {
console.log('[FullStory Event]', eventName, properties);
return originalEvent.apply(this, arguments);
};
FullStory Dashboard Validation
Step 1: Check Events List
- Log in to FullStory
- Go to Data > Events
- Look for your event in the list
- Click on it to see event volume and properties
Step 2: Search for Sessions with Event
- Go to Sessions
- Use Omnisearch: Type your event name
- Select it from autocomplete
- Review sessions that contain the event
Step 3: Inspect Event in Session
- Open a session from search results
- Look for event markers in the timeline
- Click on event marker to see properties
- Verify all properties are captured correctly
Testing Checklist
Before deploying to production:
- FullStory script loads successfully
-
FSfunction available in console (typeof FS === 'function') - Events fire on expected user actions
- Event names match taxonomy (no typos)
- Event properties have correct type suffixes
- Event properties contain expected values
- No PII (email, names, SSN) in event properties
- Events appear in FullStory dashboard within 2-3 minutes
- Events are searchable via Omnisearch
- Event properties are filterable in session search
- User identification works (events linked to user)
- No JavaScript errors in console
- Events work across all browsers (Chrome, Firefox, Safari, Edge)
- Events work on mobile devices
- Events tracked in staging match documentation
Performance Testing
Verify event tracking doesn't impact page performance:
// Measure event tracking overhead
const start = performance.now();
for (let i = 0; i < 100; i++) {
FS.event('Performance Test', {
iteration_int: i,
timestamp_date: new Date()
});
}
const end = performance.now();
console.log(`100 events tracked in ${end - start}ms`);
// Target: < 100ms for 100 events
Troubleshooting
Common Issues and Solutions
| Issue | Symptoms | Possible Causes | Solutions |
|---|---|---|---|
| Events not appearing in dashboard | Events fire in console but don't show in FullStory | - FullStory not fully loaded - Ad blocker interfering - Incorrect Org ID - Network issues |
- Check typeof FS === 'function'- Test with ad blocker disabled - Verify Org ID in Settings - Check Network tab for failed requests |
| Event properties missing | Event appears but properties are empty | - Properties not using type suffixes - Properties are undefined or null- Property names have typos |
- Add _str, _int, etc. to property names- Validate property values before tracking - Double-check property names against docs |
| Events firing multiple times | Same event tracked 2-3 times per action | - Multiple event listeners attached - Event bubbling issues - React re-renders triggering events |
- Remove previous listeners before attaching new ones - Use e.stopPropagation() if needed- Use useCallback or useMemo in React |
| FS is not defined error | JavaScript error: FS is not defined |
- FullStory script hasn't loaded yet - Script blocked by CSP - Script failed to load |
- Wrap event calls in if (window.FS) check- Update CSP to allow FullStory domains - Check Network tab for script loading errors |
| Events not searchable | Events appear in session but not in Omnisearch | - Event name contains special characters - Event not indexed yet - Case sensitivity issues |
- Use alphanumeric characters and spaces only - Wait 2-3 minutes for indexing - Check exact capitalization |
| User events not linking | Events appear but not associated with user | - FS.identify() not called- User ID is null/undefined - Events fire before identify |
- Call FS.identify() on login/page load- Validate user ID exists - Ensure identify runs before events |
| Event properties truncated | Property values cut off in FullStory | - Property value exceeds 1024 character limit - Complex objects passed as properties |
- Truncate long strings before tracking - Convert objects to strings: JSON.stringify()- Split data across multiple properties |
| Events work locally but not in production | Events track in dev but not prod | - Different FullStory Org IDs - Production CSP blocking script - Minification breaking code |
- Verify same Org ID in both environments - Add FullStory to production CSP - Test minified build before deployment |
| High event volume | Too many events consuming quota | - Events firing on scroll/mousemove - Events in loops - No event throttling |
- Remove high-frequency event tracking - Use throttling/debouncing for frequent events - Track only meaningful actions |
| Property type errors | Type mismatch warnings in console | - Wrong type suffix used - String passed to _int property- Number passed to _bool |
- Use correct suffix for data type - Convert values: parseInt(), parseFloat()- Use Boolean() for bool properties |
Debug Mode
Enable verbose logging to troubleshoot issues:
// Enable debug mode
localStorage.setItem('fs_debug', 'true');
// Reload page
// Disable debug mode
localStorage.removeItem('fs_debug');
Network Inspection
Check if events are being sent to FullStory:
- Open DevTools > Network tab
- Filter by
fullstoryorrs.fullstory.com - Trigger your event
- Look for POST requests to
/rec/bundle - Inspect request payload to see event data
Common Mistakes to Avoid
Missing type suffixes:
// Wrong
FS.event('Button Clicked', {
button_name: 'Sign Up', // Missing _str
price: 49.99 // Missing _real
});
// Correct
FS.event('Button Clicked', {
button_name_str: 'Sign Up',
price_real: 49.99
});
Tracking PII:
// Wrong - Contains PII
FS.event('Form Submitted', {
email_str: 'user@example.com', // Don't track email
full_name_str: 'John Doe', // Don't track names
ssn_str: '123-45-6789' // Never track SSN!
});
// Correct - No PII
FS.event('Form Submitted', {
form_name_str: 'contact_form',
field_count_int: 5,
newsletter_opt_in_bool: true
});
Events before FullStory loads:
// Wrong
FS.event('Page Loaded'); // FS might not exist yet
// Correct
if (window.FS) {
FS.event('Page Loaded');
} else {
console.warn('FullStory not loaded yet');
}
// Or wait for load
window.addEventListener('load', function() {
if (window.FS) {
FS.event('Page Fully Loaded');
}
});
Inconsistent naming:
// Wrong - Inconsistent
FS.event('clicked_signup_button');
FS.event('Button Clicked - Login');
FS.event('purchase complete');
// Correct - Consistent
FS.event('Button Clicked - Signup');
FS.event('Button Clicked - Login');
FS.event('Purchase Completed');
Getting Help
If issues persist:
- Check FullStory Status: Visit status.fullstory.com
- Review Documentation: help.fullstory.com
- Contact Support: support@fullstory.com with:
Next Steps:
- Data Layer Setup - Configure GTM data layer
- Cross-Domain Tracking - Track across domains
- User Identification - Set up user tracking
Additional Resources: