xiaohongshu
xiaohongshu
fullstory-analytics-events
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 ...
Full skill instructions
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:
| 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" |
Fullstory recommends semantic event naming following industry standards:
[Object] [Action]
Examples:
- "Product Added"
- "Order Completed"
- "Feature Enabled"
- "Search Performed"
- "Video Played"
Every event can include rich contextual properties that enable deep analysis:
FS('trackEvent', {
name: string, // Required: Event name (max 250 chars)
properties: object, // Required: Event properties (max 512KB)
schema?: object // Optional: Type hints for properties
});
| 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 |
| 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"] |
// 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:
// 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:
// 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:
// 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:
// 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.0})
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.0})
Why this is good:
// 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:
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,
},
})
// BAD: Order event without essential data
FS('trackEvent', {
name: 'Order Completed',
properties: {
success: true, // This tells us almost nothing!
},
})
Why this is bad:
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',
},
})
// 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:
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',
},
})
// 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:
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),
)
// 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:
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',
},
})
// 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:
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)
}
// 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,
})
// 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',
},
})
},
}
// 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})
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
}
Symptom: Events don't show in Fullstory
Common Causes:
Solutions:
Symptom: Some properties missing in Fullstory
Common Causes:
Solutions:
When helping developers implement Analytics Events:
Always emphasize:
Common mistakes to watch for:
Questions to ask developers:
Best practices to recommend:
This skill document was created to help Agent understand and guide developers in implementing Fullstory's Analytics Events API correctly for web applications.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience