Claude Code Plugins

Community-maintained marketplace

Feedback

fullstory-analytics-events

@fullstorydev/fs-skills
0
0

Comprehensive guide for implementing Fullstory's Analytics Events API (trackEvent) for web applications. Teaches proper event naming, property structuring, type handling, and e-commerce event patterns. Includes detailed good/bad examples for funnel tracking, feature usage, conversion events, and SaaS subscription flows to help developers capture meaningful business events for analytics and segmentation.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fullstory-analytics-events
version v2
description Comprehensive guide for implementing Fullstory's Analytics Events API (trackEvent) for web applications. Teaches proper event naming, property structuring, type handling, and e-commerce event patterns. Includes detailed good/bad examples for funnel tracking, feature usage, conversion events, and SaaS subscription flows to help developers capture meaningful business events for analytics and segmentation.
related_skills fullstory-page-properties, fullstory-user-properties, fullstory-element-properties, fullstory-data-scoping-decoration, fullstory-async-methods, fullstory-privacy-strategy, fullstory-ecommerce, fullstory-saas, fullstory-travel, fullstory-media-entertainment

Fullstory Analytics Events API (trackEvent)

Overview

Fullstory's Analytics Events API allows developers to send custom event data that captures meaningful user actions and business moments. Unlike automatic capture which records all interactions, trackEvent lets you define semantically meaningful events with rich context that can be used for:

  • Funnel Analysis: Track conversion steps and drop-off points
  • Feature Adoption: Measure feature usage and engagement
  • Business Metrics: Capture revenue, conversions, and KPIs
  • User Journeys: Define key moments in user workflows
  • Segmentation: Create user segments based on behaviors

Core Concepts

Events vs Properties vs Elements

API Purpose Data Type Example
trackEvent Discrete actions/moments "What happened" "Order Completed", "Feature Used"
setProperties (user) User attributes "Who they are" plan: "enterprise"
setProperties (page) Page context "Where they are" pageName: "Checkout"
Element Properties Interaction context "What they clicked" productId: "SKU-123"

Event Naming Conventions

Fullstory recommends semantic event naming following industry standards:

[Object] [Action]

Examples:
- "Product Added"
- "Order Completed"
- "Feature Enabled"
- "Search Performed"
- "Video Played"

Event Properties

Every event can include rich contextual properties that enable deep analysis:

  • Product details for e-commerce events
  • Feature names for adoption tracking
  • Revenue values for business metrics
  • Custom dimensions for segmentation

API Reference

Basic Syntax

FS('trackEvent', {
  name: string,          // Required: Event name (max 250 chars)
  properties: object,    // Required: Event properties (max 512KB)
  schema?: object        // Optional: Type hints for properties
});

Parameters

Parameter Type Required Description
name string Yes Event name, max 250 characters
properties object Yes Key/value pairs of event data
schema object No Explicit type inference for properties

Supported Property Types

Type Description Examples
str String value "blue", "premium"
strs Array of strings ["red", "blue", "green"]
int Integer 42, -5, 0
ints Array of integers [1, 2, 3]
real Float/decimal 99.99, -3.14
reals Array of reals [10.5, 20.0]
bool Boolean true, false
bools Array of booleans [true, false]
date ISO8601 date "2024-01-15T00:00:00Z"
dates Array of dates ["2024-01-01", "2024-02-01"]

Rate Limits

  • Sustained: 60 calls per user per page per minute
  • Burst: 40 calls per second

Size Limits

  • Event name: Max 250 characters
  • Properties payload: Max 512KB
  • Arrays of objects: NOT indexed (except Order Completed)

✅ GOOD IMPLEMENTATION EXAMPLES

Example 1: E-commerce - Product Added to Cart

// GOOD: Comprehensive product add event
function handleAddToCart(product, quantity, source) {
  FS('trackEvent', {
    name: 'Product Added',
    properties: {
      // Product identification
      product_id: product.id,
      sku: product.sku,
      name: product.name,
      brand: product.brand,
      
      // Categorization
      category: product.category,
      subcategory: product.subcategory,
      
      // Pricing
      price: product.price,
      currency: 'USD',
      
      // Cart context
      quantity: quantity,
      cart_id: getCartId(),
      
      // Attribution
      position: product.listPosition,
      list_name: source.listName,
      
      // Product attributes
      variant: product.selectedVariant,
      size: product.selectedSize,
      color: product.selectedColor,
      
      // Promotion tracking
      coupon: getActiveCoupon(),
      
      // URLs for reference
      url: product.url,
      image_url: product.imageUrl
    },
    schema: {
      price: 'real',
      quantity: 'int',
      position: 'int'
    }
  });
}

Why this is good:

  • ✅ Follows standard e-commerce event naming
  • ✅ Includes product identification (id, sku)
  • ✅ Captures pricing with currency
  • ✅ Includes attribution context (position, list)
  • ✅ Proper typing for numeric fields

Example 2: SaaS - Feature Usage Tracking

// GOOD: Track feature usage with context
function trackFeatureUsage(featureName, context = {}) {
  FS('trackEvent', {
    name: 'Feature Used',
    properties: {
      // Feature identification
      feature_name: featureName,
      feature_category: getFeatureCategory(featureName),
      
      // Usage context
      usage_context: context.trigger || 'direct',
      entry_point: context.entryPoint || window.location.pathname,
      
      // User's feature state
      is_first_use: !hasUsedFeature(featureName),
      times_used_today: getDailyUsageCount(featureName),
      times_used_total: getTotalUsageCount(featureName),
      
      // Session context
      session_feature_count: getSessionFeatureCount(),
      time_in_session: getTimeInSession(),
      
      // Feature-specific data
      ...context.metadata
    },
    schema: {
      is_first_use: 'bool',
      times_used_today: 'int',
      times_used_total: 'int',
      session_feature_count: 'int',
      time_in_session: 'int'
    }
  });
}

// Usage
trackFeatureUsage('advanced_export', {
  trigger: 'keyboard_shortcut',
  entryPoint: '/dashboard',
  metadata: {
    export_format: 'csv',
    row_count: 1500
  }
});

Why this is good:

  • ✅ Tracks both feature and context
  • ✅ Captures first-use for adoption analysis
  • ✅ Includes frequency metrics
  • ✅ Flexible metadata for feature-specific data

Example 3: Subscription/Billing Events

// GOOD: Track subscription lifecycle events
class SubscriptionTracker {
  
  trackTrialStarted(trial) {
    FS('trackEvent', {
      name: 'Trial Started',
      properties: {
        trial_plan: trial.plan,
        trial_duration_days: trial.durationDays,
        trial_features: trial.includedFeatures,
        source: trial.acquisitionSource,
        started_at: new Date().toISOString()
      },
      schema: {
        trial_duration_days: 'int',
        trial_features: 'strs',
        started_at: 'date'
      }
    });
  }
  
  trackSubscriptionStarted(subscription) {
    FS('trackEvent', {
      name: 'Subscription Started',
      properties: {
        plan_name: subscription.plan,
        plan_tier: subscription.tier,
        billing_cycle: subscription.billingCycle,
        price: subscription.price,
        currency: subscription.currency,
        seats: subscription.seats,
        mrr: subscription.mrr,
        arr: subscription.arr,
        trial_converted: subscription.wasTrialing,
        payment_method: subscription.paymentMethod,
        promo_code: subscription.promoCode
      },
      schema: {
        price: 'real',
        seats: 'int',
        mrr: 'real',
        arr: 'real',
        trial_converted: 'bool'
      }
    });
  }
  
  trackPlanChanged(change) {
    FS('trackEvent', {
      name: 'Plan Changed',
      properties: {
        from_plan: change.fromPlan,
        to_plan: change.toPlan,
        from_price: change.fromPrice,
        to_price: change.toPrice,
        price_change: change.toPrice - change.fromPrice,
        change_type: change.toPrice > change.fromPrice ? 'upgrade' : 'downgrade',
        from_seats: change.fromSeats,
        to_seats: change.toSeats,
        effective_date: change.effectiveDate,
        reason: change.reason
      },
      schema: {
        from_price: 'real',
        to_price: 'real',
        price_change: 'real',
        from_seats: 'int',
        to_seats: 'int',
        effective_date: 'date'
      }
    });
  }
  
  trackChurnEvent(churn) {
    FS('trackEvent', {
      name: 'Subscription Cancelled',
      properties: {
        plan_name: churn.plan,
        tenure_days: churn.tenureDays,
        lifetime_value: churn.ltv,
        cancel_reason: churn.reason,
        cancel_feedback: churn.feedback,
        was_paying: churn.wasPaying,
        final_mrr: churn.finalMrr,
        churn_type: churn.immediate ? 'immediate' : 'end_of_period'
      },
      schema: {
        tenure_days: 'int',
        lifetime_value: 'real',
        was_paying: 'bool',
        final_mrr: 'real'
      }
    });
  }
}

Why this is good:

  • ✅ Captures full subscription lifecycle
  • ✅ Includes revenue metrics (MRR, ARR, LTV)
  • ✅ Tracks upgrade/downgrade patterns
  • ✅ Captures churn reasons for analysis

Example 4: Search and Discovery

// GOOD: Track search behavior
function trackSearch(searchData) {
  FS('trackEvent', {
    name: 'Search Performed',
    properties: {
      // Query details
      search_term: searchData.query,
      search_type: searchData.type,  // 'keyword', 'filter', 'voice'
      
      // Results
      results_count: searchData.results.length,
      has_results: searchData.results.length > 0,
      
      // Filters applied
      filters_applied: Object.keys(searchData.filters),
      filter_count: Object.keys(searchData.filters).length,
      
      // Sorting
      sort_by: searchData.sortBy,
      sort_order: searchData.sortOrder,
      
      // Pagination
      page_number: searchData.page,
      results_per_page: searchData.perPage,
      
      // Performance
      response_time_ms: searchData.responseTime,
      
      // Context
      search_location: searchData.location,  // 'header', 'page', 'modal'
      is_refinement: searchData.isRefinement
    },
    schema: {
      results_count: 'int',
      has_results: 'bool',
      filters_applied: 'strs',
      filter_count: 'int',
      page_number: 'int',
      results_per_page: 'int',
      response_time_ms: 'int',
      is_refinement: 'bool'
    }
  });
}

// Track when user clicks a search result
function trackSearchResultClick(result, searchContext) {
  FS('trackEvent', {
    name: 'Search Result Clicked',
    properties: {
      search_term: searchContext.query,
      result_position: result.position,
      result_id: result.id,
      result_type: result.type,
      results_count: searchContext.totalResults,
      page_number: searchContext.page
    },
    schema: {
      result_position: 'int',
      results_count: 'int',
      page_number: 'int'
    }
  });
}

Why this is good:

  • ✅ Captures search intent (query, filters)
  • ✅ Tracks result quality (count, has_results)
  • ✅ Measures performance (response_time)
  • ✅ Connects searches to clicks

Example 5: Form/Funnel Tracking

// GOOD: Multi-step form/funnel tracking
class FunnelTracker {
  constructor(funnelName, steps) {
    this.funnelName = funnelName;
    this.steps = steps;
    this.startTime = null;
    this.stepTimes = {};
  }
  
  startFunnel(context = {}) {
    this.startTime = Date.now();
    
    FS('trackEvent', {
      name: `${this.funnelName} Started`,
      properties: {
        funnel_name: this.funnelName,
        total_steps: this.steps.length,
        entry_point: window.location.pathname,
        ...context
      },
      schema: {
        total_steps: 'int'
      }
    });
  }
  
  completeStep(stepIndex, stepData = {}) {
    const stepName = this.steps[stepIndex];
    const now = Date.now();
    const stepDuration = this.stepTimes[stepIndex - 1] 
      ? now - this.stepTimes[stepIndex - 1]
      : now - this.startTime;
    
    this.stepTimes[stepIndex] = now;
    
    FS('trackEvent', {
      name: `${this.funnelName} Step Completed`,
      properties: {
        funnel_name: this.funnelName,
        step_number: stepIndex + 1,
        step_name: stepName,
        total_steps: this.steps.length,
        step_duration_ms: stepDuration,
        time_in_funnel_ms: now - this.startTime,
        ...stepData
      },
      schema: {
        step_number: 'int',
        total_steps: 'int',
        step_duration_ms: 'int',
        time_in_funnel_ms: 'int'
      }
    });
  }
  
  completeFunnel(result = {}) {
    const totalDuration = Date.now() - this.startTime;
    
    FS('trackEvent', {
      name: `${this.funnelName} Completed`,
      properties: {
        funnel_name: this.funnelName,
        total_steps: this.steps.length,
        total_duration_ms: totalDuration,
        ...result
      },
      schema: {
        total_steps: 'int',
        total_duration_ms: 'int'
      }
    });
  }
  
  abandonFunnel(stepIndex, reason = 'unknown') {
    FS('trackEvent', {
      name: `${this.funnelName} Abandoned`,
      properties: {
        funnel_name: this.funnelName,
        abandoned_at_step: stepIndex + 1,
        abandoned_step_name: this.steps[stepIndex],
        total_steps: this.steps.length,
        time_in_funnel_ms: Date.now() - this.startTime,
        abandon_reason: reason
      },
      schema: {
        abandoned_at_step: 'int',
        total_steps: 'int',
        time_in_funnel_ms: 'int'
      }
    });
  }
}

// Usage
const checkoutFunnel = new FunnelTracker('Checkout', [
  'Cart Review',
  'Shipping Info',
  'Payment Info',
  'Confirmation'
]);

checkoutFunnel.startFunnel({ cart_value: 150.00 });
checkoutFunnel.completeStep(0, { items_count: 3 });
checkoutFunnel.completeStep(1, { shipping_method: 'express' });
checkoutFunnel.completeStep(2, { payment_method: 'credit_card' });
checkoutFunnel.completeFunnel({ order_id: 'ORD-123', total: 165.00 });

Why this is good:

  • ✅ Tracks full funnel journey
  • ✅ Measures time per step
  • ✅ Captures abandonment with context
  • ✅ Reusable for any multi-step flow

❌ BAD IMPLEMENTATION EXAMPLES

Example 1: Event Name Too Generic

// BAD: Vague event names
FS('trackEvent', {
  name: 'click',  // BAD: Too generic
  properties: {
    element: 'button'
  }
});

FS('trackEvent', {
  name: 'action',  // BAD: Meaningless
  properties: {
    type: 'purchase'
  }
});

Why this is bad:

  • ❌ "click" doesn't describe what happened
  • ❌ Can't build meaningful funnels
  • ❌ No semantic meaning
  • ❌ Hard to analyze

CORRECTED VERSION:

// GOOD: Semantic event names
FS('trackEvent', {
  name: 'Add to Cart Button Clicked',
  properties: {
    product_id: 'SKU-123',
    button_location: 'product_page'
  }
});

FS('trackEvent', {
  name: 'Order Completed',
  properties: {
    order_id: 'ORD-456',
    total: 99.99
  }
});

Example 2: Missing Critical Properties

// BAD: Order event without essential data
FS('trackEvent', {
  name: 'Order Completed',
  properties: {
    success: true  // This tells us almost nothing!
  }
});

Why this is bad:

  • ❌ No order ID for reference
  • ❌ No revenue data for metrics
  • ❌ No product information
  • ❌ Can't do meaningful analysis

CORRECTED VERSION:

// GOOD: Comprehensive order event
FS('trackEvent', {
  name: 'Order Completed',
  properties: {
    order_id: order.id,
    revenue: order.total,
    currency: order.currency,
    item_count: order.items.length,
    shipping_method: order.shipping.method,
    payment_method: order.payment.method,
    coupon_code: order.coupon,
    discount_amount: order.discount,
    is_first_order: customer.orderCount === 1
  },
  schema: {
    revenue: 'real',
    item_count: 'int',
    discount_amount: 'real',
    is_first_order: 'bool'
  }
});

Example 3: Type Mismatches

// BAD: Wrong value formats
FS('trackEvent', {
  name: 'Product Purchased',
  properties: {
    price: '$49.99',           // BAD: Currency symbol
    quantity: '3 items',       // BAD: Text in number
    in_stock: 'yes',           // BAD: String instead of boolean
    purchase_date: 'today'     // BAD: Not ISO8601
  },
  schema: {
    price: 'real',
    quantity: 'int',
    in_stock: 'bool',
    purchase_date: 'date'
  }
});

Why this is bad:

  • ❌ '$49.99' won't parse as real
  • ❌ '3 items' won't parse as int
  • ❌ 'yes' is not a valid boolean
  • ❌ 'today' is not ISO8601

CORRECTED VERSION:

// GOOD: Properly formatted values
FS('trackEvent', {
  name: 'Product Purchased',
  properties: {
    price: 49.99,
    currency: 'USD',
    quantity: 3,
    in_stock: true,
    purchase_date: new Date().toISOString()
  },
  schema: {
    price: 'real',
    quantity: 'int',
    in_stock: 'bool',
    purchase_date: 'date'
  }
});

Example 4: Tracking Too Many Events

// BAD: Tracking every micro-interaction
document.addEventListener('mousemove', (e) => {
  FS('trackEvent', {
    name: 'Mouse Moved',
    properties: { x: e.clientX, y: e.clientY }
  });
});

document.addEventListener('scroll', () => {
  FS('trackEvent', {
    name: 'Page Scrolled',
    properties: { position: window.scrollY }
  });
});

Why this is bad:

  • ❌ Will hit rate limits immediately
  • ❌ Drowns out meaningful events
  • ❌ No analytical value
  • ❌ Fullstory already captures these automatically

CORRECTED VERSION:

// GOOD: Track meaningful scroll milestones only
const scrollMilestones = [25, 50, 75, 100];
const trackedMilestones = new Set();

window.addEventListener('scroll', throttle(() => {
  const scrollPercent = Math.round(
    (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100
  );
  
  scrollMilestones.forEach(milestone => {
    if (scrollPercent >= milestone && !trackedMilestones.has(milestone)) {
      trackedMilestones.add(milestone);
      
      FS('trackEvent', {
        name: 'Scroll Depth Reached',
        properties: {
          depth_percent: milestone,
          page: window.location.pathname
        }
      });
    }
  });
}, 250));

Example 5: Event Name Too Long

// BAD: Event name exceeds 250 character limit
FS('trackEvent', {
  name: 'User clicked on the primary call-to-action button located in the hero section of the landing page after scrolling past the feature comparison table and reading the customer testimonials section which indicates strong purchase intent',
  properties: { clicked: true }
});

Why this is bad:

  • ❌ Exceeds 250 character limit
  • ❌ Event will be truncated or fail
  • ❌ Context belongs in properties, not name

CORRECTED VERSION:

// GOOD: Concise name, rich properties
FS('trackEvent', {
  name: 'CTA Button Clicked',
  properties: {
    button_location: 'hero_section',
    page_type: 'landing_page',
    scroll_depth_before_click: 75,
    sections_viewed: ['features', 'comparison', 'testimonials'],
    intent_signals: ['high_engagement', 'price_check']
  },
  schema: {
    scroll_depth_before_click: 'int',
    sections_viewed: 'strs',
    intent_signals: 'strs'
  }
});

Example 6: Duplicate Events

// BAD: Sending same event multiple times
function handleFormSubmit(formData) {
  // This might fire multiple times due to double-clicks or re-renders
  FS('trackEvent', {
    name: 'Form Submitted',
    properties: formData
  });
}

// Without proper deduplication
submitButton.addEventListener('click', handleFormSubmit);
form.addEventListener('submit', handleFormSubmit);  // Double event!

Why this is bad:

  • ❌ Same event fires twice
  • ❌ Inflates metrics
  • ❌ Creates confusing analytics

CORRECTED VERSION:

// GOOD: Deduplicate events
const eventTracker = {
  recentEvents: new Map(),
  
  track(name, properties, dedupeKey = null) {
    const key = dedupeKey || `${name}-${JSON.stringify(properties)}`;
    const now = Date.now();
    const lastSent = this.recentEvents.get(key);
    
    // Don't send if same event sent within 1 second
    if (lastSent && (now - lastSent) < 1000) {
      return;
    }
    
    this.recentEvents.set(key, now);
    
    FS('trackEvent', {
      name,
      properties
    });
  }
};

// Usage
function handleFormSubmit(formData) {
  eventTracker.track('Form Submitted', formData, formData.formId);
}

COMMON IMPLEMENTATION PATTERNS

Pattern 1: Event Tracking Service

// Centralized event tracking with validation
class EventTracker {
  constructor() {
    this.eventSchemas = new Map();
  }
  
  // Register event schema for validation
  registerEvent(name, schema) {
    this.eventSchemas.set(name, schema);
  }
  
  // Track event with automatic schema
  track(name, properties) {
    const schema = this.eventSchemas.get(name);
    
    const eventPayload = {
      name,
      properties: {
        ...properties,
        tracked_at: new Date().toISOString(),
        page_url: window.location.href
      }
    };
    
    if (schema) {
      eventPayload.schema = schema;
    }
    
    FS('trackEvent', eventPayload);
  }
}

// Setup
const tracker = new EventTracker();

tracker.registerEvent('Order Completed', {
  revenue: 'real',
  item_count: 'int',
  is_first_order: 'bool'
});

// Usage
tracker.track('Order Completed', {
  order_id: 'ORD-123',
  revenue: 99.99,
  item_count: 3,
  is_first_order: false
});

Pattern 2: E-commerce Event Library

// Standard e-commerce events
const ecommerceEvents = {
  
  productViewed(product) {
    FS('trackEvent', {
      name: 'Product Viewed',
      properties: {
        product_id: product.id,
        sku: product.sku,
        name: product.name,
        category: product.category,
        price: product.price,
        currency: product.currency,
        brand: product.brand,
        variant: product.variant
      },
      schema: { price: 'real' }
    });
  },
  
  productAdded(product, cartId, quantity = 1) {
    FS('trackEvent', {
      name: 'Product Added',
      properties: {
        product_id: product.id,
        sku: product.sku,
        name: product.name,
        category: product.category,
        price: product.price,
        currency: product.currency,
        quantity: quantity,
        cart_id: cartId
      },
      schema: { price: 'real', quantity: 'int' }
    });
  },
  
  checkoutStarted(cart) {
    FS('trackEvent', {
      name: 'Checkout Started',
      properties: {
        cart_id: cart.id,
        value: cart.total,
        currency: cart.currency,
        item_count: cart.items.length,
        coupon: cart.coupon
      },
      schema: { value: 'real', item_count: 'int' }
    });
  },
  
  orderCompleted(order) {
    FS('trackEvent', {
      name: 'Order Completed',
      properties: {
        order_id: order.id,
        revenue: order.revenue,
        tax: order.tax,
        shipping: order.shipping,
        total: order.total,
        currency: order.currency,
        item_count: order.items.length,
        coupon: order.coupon,
        discount: order.discount,
        payment_method: order.paymentMethod
      },
      schema: {
        revenue: 'real',
        tax: 'real',
        shipping: 'real',
        total: 'real',
        item_count: 'int',
        discount: 'real'
      }
    });
  }
};

Pattern 3: Timed Event Tracking

// Track events with timing
class TimedEventTracker {
  timers = new Map();
  
  start(eventName, properties = {}) {
    this.timers.set(eventName, {
      startTime: Date.now(),
      properties
    });
  }
  
  complete(eventName, additionalProperties = {}) {
    const timer = this.timers.get(eventName);
    if (!timer) return;
    
    const duration = Date.now() - timer.startTime;
    
    FS('trackEvent', {
      name: eventName,
      properties: {
        ...timer.properties,
        ...additionalProperties,
        duration_ms: duration,
        completed: true
      },
      schema: {
        duration_ms: 'int',
        completed: 'bool'
      }
    });
    
    this.timers.delete(eventName);
  }
  
  cancel(eventName, reason = 'cancelled') {
    const timer = this.timers.get(eventName);
    if (!timer) return;
    
    const duration = Date.now() - timer.startTime;
    
    FS('trackEvent', {
      name: eventName,
      properties: {
        ...timer.properties,
        duration_ms: duration,
        completed: false,
        cancel_reason: reason
      },
      schema: {
        duration_ms: 'int',
        completed: 'bool'
      }
    });
    
    this.timers.delete(eventName);
  }
}

// Usage
const timedTracker = new TimedEventTracker();

timedTracker.start('Video Watched', { video_id: 'VID-123' });
// ... user watches video ...
timedTracker.complete('Video Watched', { percent_watched: 85 });

ASYNC VERSION

For cases where you need to confirm the event was sent:

try {
  await FS('trackEventAsync', {
    name: 'Order Completed',
    properties: {
      order_id: order.id,
      revenue: order.total
    }
  });
  console.log('Event sent successfully');
} catch (error) {
  console.error('Event failed:', error);
  // Fallback: queue for retry
}

TROUBLESHOOTING

Events Not Appearing

Symptom: Events don't show in Fullstory

Common Causes:

  1. ❌ Fullstory script not loaded
  2. ❌ Event name exceeds 250 chars
  3. ❌ Properties exceed 512KB
  4. ❌ Rate limits exceeded

Solutions:

  • ✅ Verify FS function is available
  • ✅ Keep event names concise
  • ✅ Reduce property payload size
  • ✅ Throttle high-frequency events

Events Have Missing Properties

Symptom: Some properties missing in Fullstory

Common Causes:

  1. ❌ Property values are undefined
  2. ❌ Type mismatches with schema
  3. ❌ Unsupported array types (arrays of objects)

Solutions:

  • ✅ Validate properties before sending
  • ✅ Match value formats to schema types
  • ✅ Flatten object arrays

LIMITS AND CONSTRAINTS

Size Limits

  • Event name: 250 characters
  • Properties payload: 512KB

Rate Limits

  • Sustained: 60 calls per user per page per minute
  • Burst: 40 calls per second

Array Handling

  • Arrays of primitives (strings, numbers): ✅ Indexed
  • Arrays of objects: ❌ NOT indexed (except Order Completed)

KEY TAKEAWAYS FOR AGENT

When helping developers implement Analytics Events:

  1. Always emphasize:

    • Use semantic event names (Object + Action)
    • Include meaningful properties
    • Use schema for non-string types
    • Don't track what Fullstory captures automatically
  2. Common mistakes to watch for:

    • Generic event names ("click", "action")
    • Missing critical properties (order_id, revenue)
    • Type format mismatches
    • Over-tracking micro-interactions
  3. Questions to ask developers:

    • What business questions will this event answer?
    • What properties are needed for segmentation?
    • How often will this event fire?
    • Is this redundant with auto-captured data?
  4. Best practices to recommend:

    • Follow e-commerce/SaaS event standards
    • Include context (page, source, timing)
    • Deduplicate rapid-fire events
    • Test events appear in Fullstory

REFERENCE LINKS


This skill document was created to help Agent understand and guide developers in implementing Fullstory's Analytics Events API correctly for web applications.