xiaohongshu
xiaohongshu
fullstory-page-properties
Comprehensive guide for implementing Fullstory's Page Properties API (setProperties with type 'page') for web applications. Teaches proper page naming, contextual data capture, SPA navigation handling, and session-scoped properties. Includes detailed good/bad examples for search results, checkout...
Full skill instructions
Fullstory's Page Properties API allows developers to capture contextual information about the current page that enriches sessions for search, filtering, segmentation, and journey analysis. Unlike user properties that persist across sessions, page properties are session-scoped and reset when the URL path changes.
Key use cases:
Page Load → setProperties(page) → Properties Active → URL Change → Properties Reset
↓ ↓
Same page, call again Merged with existing props
| Behavior | Description |
|---|---|
| Session-scoped | Properties persist for the current page until URL path changes |
| Merge on repeat calls | Multiple calls on same page merge properties |
| Reset on navigation | Properties clear when URL host or path changes |
| pageName special field | Creates named Pages for use in Journeys |
| Type | Scope | Persists | Best For |
|---|---|---|---|
| User Properties | User | Across sessions | User attributes, plan, role |
| Page Properties | Page | Until URL change | Page context, search, filters |
| Element Properties | Element | Interaction | Click-level context |
| Event Properties | Event | One event | Action-specific data |
Fullstory limits you to 1,000 unique pageName values. Once exceeded, additional page names are silently ignored.
The Strategy: Use a generic page name + properties for variations.
❌ BAD: Unique pageName for every product
pageName: "iPhone 15 Pro Max 256GB Space Black"
pageName: "iPhone 15 Pro Max 512GB Natural Titanium"
pageName: "Samsung Galaxy S24 Ultra 256GB..."
→ Exhausts 1,000 limit quickly!
✅ GOOD: Generic pageName + properties for context
pageName: "Product Detail"
+ productName: "iPhone 15 Pro Max"
+ productCategory: "Smartphones"
+ productBrand: "Apple"
+ productPrice: 1199
| Scenario | ❌ Wrong (unique pageName) | ✅ Right (generic + properties) |
|---|---|---|
| Product pages | "iPhone 15 Pro Detail" | pageName: "Product Detail" + productName property |
| User profiles | "John Smith's Profile" | pageName: "User Profile" + profileUserId property |
| Article pages | "How to Cook Pasta" | pageName: "Article" + articleTitle, articleCategory properties |
| Search results | "Search: blue shoes" | pageName: "Search Results" + searchTerm property |
| Category pages | "Men's Running Shoes" | pageName: "Category" + categoryName, categoryPath properties |
Think of it this way:
pageNamedefines the type of page (e.g., "Product Detail", "Checkout", "Search Results"). Properties describe the specific instance (which product, what search term, etc.). This gives you unlimited variation tracking while staying well under the 1,000 limit.
FS('setProperties', {
type: 'page',
properties: object, // Required: Key/value pairs
})
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be 'page' for page properties |
properties | object | Yes | Key/value pairs of page data |
| Field | Behavior |
|---|---|
pageName | Creates a named Page used across all of Fullstory - not just Journeys. Limited to 1,000 unique values. Takes precedence over URL-based page definitions. |
pageName and page properties aren't just for Journeys - they enrich every event that occurs on that page:
| Fullstory Feature | How Page Properties Help |
|---|---|
| Search | Find sessions where pageName = "Checkout" AND cartValue > 500 |
| Segments | Create segments like "Users who visited Product pages with priceRange = 'premium'" |
| Funnels | Build funnels using pageName steps: "Home → Category → Product Detail → Cart → Checkout" |
| Journeys | Map user flows across named pages |
| Metrics | Track conversion rates per page type, filter by page properties |
| Dashboards | Break down metrics by pageName or page properties |
| Event Analysis | Every click, rage click, error on a page carries the page context |
// When you set page properties...
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Product Detail',
productCategory: 'Electronics',
productPrice: 999,
inStock: true,
},
})
// ...every subsequent event on this page (clicks, errors, custom events)
// is automatically enriched with this context, enabling queries like:
// - "Show me rage clicks on Product Detail pages where productPrice > 500"
// - "Find errors on out-of-stock product pages"
// - "Compare conversion rates: Electronics vs Clothing product pages"
// GOOD: Comprehensive search results context
function setSearchPageProperties(searchResults) {
FS('setProperties', {
type: 'page',
properties: {
// Page naming for Journeys
pageName: 'Search Results',
// Search context
searchTerm: searchResults.query,
searchType: searchResults.type, // 'keyword', 'category', 'tag'
// Results info
resultsCount: searchResults.total,
resultsShown: searchResults.items.length,
hasResults: searchResults.total > 0,
// Filters applied
activeFilters: Object.keys(searchResults.filters),
filterCount: Object.keys(searchResults.filters).length,
priceRangeMin: searchResults.filters.price?.min,
priceRangeMax: searchResults.filters.price?.max,
categoryFilter: searchResults.filters.category,
// Sorting
sortBy: searchResults.sortBy,
sortOrder: searchResults.sortOrder,
// Pagination
currentPage: searchResults.page,
totalPages: searchResults.totalPages,
},
})
}
// Call when search results load
const results = await performSearch(query, filters)
setSearchPageProperties(results)
Why this is good:
// GOOD: Product page with full context
function setProductPageProperties(product) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Product Detail',
// Product identification
productId: product.id,
productSku: product.sku,
productName: product.name,
// Categorization
category: product.category,
subcategory: product.subcategory,
brand: product.brand,
// Pricing
price: product.price,
originalPrice: product.originalPrice,
onSale: product.price < product.originalPrice,
discountPercent: product.discountPercent,
currency: product.currency,
// Inventory
inStock: product.inStock,
stockLevel: product.stockQuantity,
// Ratings
averageRating: product.rating.average,
reviewCount: product.rating.count,
// Variants
availableColors: product.variants.colors,
availableSizes: product.variants.sizes,
},
})
}
// Call when product page loads
setProductPageProperties(productData)
Why this is good:
// GOOD: Checkout with step and cart context
class CheckoutPageProperties {
setCartReviewStep(cart) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Checkout',
checkoutStep: 1,
checkoutStepName: 'Cart Review',
cartId: cart.id,
cartValue: cart.subtotal,
cartItemCount: cart.items.length,
hasCoupon: !!cart.coupon,
couponCode: cart.coupon,
discountAmount: cart.discount,
estimatedShipping: cart.shipping.estimate,
estimatedTax: cart.tax.estimate,
estimatedTotal: cart.total,
},
})
}
setShippingStep(cart, shippingOptions) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Checkout',
checkoutStep: 2,
checkoutStepName: 'Shipping',
cartValue: cart.subtotal,
cartItemCount: cart.items.length,
shippingOptionsCount: shippingOptions.length,
cheapestShipping: shippingOptions[0]?.price,
fastestShipping: shippingOptions.find((o) => o.fastest)?.name,
},
})
}
setPaymentStep(cart, selectedShipping) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Checkout',
checkoutStep: 3,
checkoutStepName: 'Payment',
cartValue: cart.subtotal,
shippingMethod: selectedShipping.name,
shippingCost: selectedShipping.price,
totalBeforePayment: cart.total,
paymentMethodsAvailable: getAvailablePaymentMethods(),
},
})
}
setConfirmationStep(order) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Order Confirmation',
checkoutStep: 4,
checkoutStepName: 'Confirmation',
orderId: order.id,
orderTotal: order.total,
paymentMethod: order.paymentMethod,
shippingMethod: order.shippingMethod,
estimatedDelivery: order.estimatedDelivery,
},
})
}
}
Why this is good:
// GOOD: Handle SPA route changes with page properties
class SPAPagePropertyManager {
constructor() {
this.routeHandlers = new Map()
this.setupRouteListener()
}
setupRouteListener() {
// For React Router, Vue Router, etc.
window.addEventListener('popstate', () => this.handleRouteChange())
// Intercept pushState/replaceState
const originalPushState = history.pushState
history.pushState = (...args) => {
originalPushState.apply(history, args)
this.handleRouteChange()
}
}
registerRoute(pathPattern, handler) {
this.routeHandlers.set(pathPattern, handler)
}
handleRouteChange() {
const path = window.location.pathname
// Find matching route handler
for (const [pattern, handler] of this.routeHandlers) {
const match = path.match(pattern)
if (match) {
const properties = handler(match, path)
if (properties) {
FS('setProperties', {
type: 'page',
properties,
})
}
return
}
}
// Default page properties
FS('setProperties', {
type: 'page',
properties: {
pageName: this.inferPageName(path),
path: path,
},
})
}
inferPageName(path) {
const segments = path.split('/').filter(Boolean)
return segments.length > 0
? segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
: 'Home'
}
}
// Setup
const pageManager = new SPAPagePropertyManager()
pageManager.registerRoute(/^\/products\/([^/]+)$/, (match) => ({
pageName: 'Product Detail',
productSlug: match[1],
}))
pageManager.registerRoute(/^\/search$/, () => ({
pageName: 'Search Results',
// Additional properties set by search handler
}))
pageManager.registerRoute(/^\/checkout\/(.+)$/, (match) => ({
pageName: 'Checkout',
checkoutStepSlug: match[1],
}))
Why this is good:
// GOOD: Dashboard with view state properties
function setDashboardPageProperties(dashboardState) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Dashboard',
// View configuration
dashboardView: dashboardState.activeView,
dateRange: dashboardState.dateRange.label,
dateRangeStart: dashboardState.dateRange.start,
dateRangeEnd: dashboardState.dateRange.end,
// Active filters
segmentFilter: dashboardState.segment,
channelFilter: dashboardState.channel,
regionFilter: dashboardState.region,
// Widget state
visibleWidgets: dashboardState.widgets.map((w) => w.id),
widgetCount: dashboardState.widgets.length,
// Data state
dataLoaded: dashboardState.isLoaded,
hasData: dashboardState.hasData,
recordCount: dashboardState.recordCount,
},
})
}
// Update properties when dashboard state changes
dashboardStore.subscribe((state) => {
setDashboardPageProperties(state)
})
Why this is good:
// GOOD: Article page with content metadata
function setArticlePageProperties(article) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Article',
// Content identification
articleId: article.id,
articleSlug: article.slug,
articleTitle: article.title,
// Categorization
category: article.category,
tags: article.tags,
contentType: article.type, // 'blog', 'tutorial', 'news'
// Author info
authorId: article.author.id,
authorName: article.author.name,
// Dates
publishDate: article.publishedAt,
lastUpdated: article.updatedAt,
// Content metrics
wordCount: article.wordCount,
estimatedReadTime: article.readTime,
hasVideo: article.hasVideo,
imageCount: article.images.length,
// Engagement indicators
commentCount: article.comments.count,
likeCount: article.likes,
shareCount: article.shares,
},
})
}
// Call when article loads
setArticlePageProperties(articleData)
Why this is good:
// BAD: No pageName - can't use in Journeys
FS('setProperties', {
type: 'page',
properties: {
category: 'Electronics',
sortBy: 'price',
},
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Include pageName
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Category Page',
category: 'Electronics',
sortBy: 'price',
},
})
// BAD: Using 'user' type for page data
FS('setProperties', {
type: 'user', // Wrong type!
properties: {
currentPage: 'Search Results',
searchTerm: 'laptops',
},
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Use 'page' type
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Search Results',
searchTerm: 'laptops',
},
})
// BAD: Dynamic pageName creating too many unique values
function setProductPage(product) {
FS('setProperties', {
type: 'page',
properties: {
// BAD: Product name as pageName creates 1000s of unique values
pageName: product.name,
productId: product.id,
},
})
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Generic pageName, specific properties
function setProductPage(product) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Product Detail', // Generic
productName: product.name, // Specific as property
productId: product.id,
category: product.category,
},
})
}
// BAD: Multiple different pageNames on same page
function updateFilters(filters) {
FS('setProperties', {
type: 'page',
properties: {
pageName: `Search - ${filters.category}`, // BAD: Changes pageName
filters: Object.keys(filters),
},
})
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Set pageName once, update other properties
function setSearchPage(initialData) {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Search Results', // Set once
searchTerm: initialData.query,
},
})
}
function updateFilters(filters) {
// Update properties without pageName
FS('setProperties', {
type: 'page',
properties: {
activeCategory: filters.category,
filterCount: Object.keys(filters).length,
filters: Object.keys(filters),
},
})
}
// BAD: Putting user-specific data in page properties
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Dashboard',
userId: user.id, // BAD: User data
userEmail: user.email, // BAD: User data
userPlan: user.plan, // BAD: User data
},
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Separate user and page data
FS('setIdentity', {
uid: user.id,
properties: {
email: user.email,
plan: user.plan,
},
})
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Dashboard',
dashboardView: 'analytics',
dateRange: 'last30days',
},
})
// BAD: Only setting properties on initial load
document.addEventListener('DOMContentLoaded', () => {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Home',
},
})
// Properties never updated for SPA navigation!
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Handle SPA navigation
function setPageProperties(route) {
FS('setProperties', {
type: 'page',
properties: {
pageName: route.name,
...route.properties,
},
})
}
// Call on initial load
setPageProperties(getCurrentRoute())
// Call on navigation
router.on('routeChange', (route) => {
setPageProperties(route)
})
// Centralized page property management
class PagePropertyManager {
constructor() {
this.currentPageName = null
this.baseProperties = {}
}
// Initialize page with base properties
initialize(pageName, baseProps = {}) {
this.currentPageName = pageName
this.baseProperties = baseProps
FS('setProperties', {
type: 'page',
properties: {
pageName,
...baseProps,
pageLoadTime: new Date().toISOString(),
},
})
}
// Add additional properties (merges with existing)
addProperties(additionalProps) {
FS('setProperties', {
type: 'page',
properties: additionalProps,
})
}
// Update specific property
updateProperty(key, value) {
FS('setProperties', {
type: 'page',
properties: {
[key]: value,
},
})
}
}
// Usage
const pageProps = new PagePropertyManager()
// On page load
pageProps.initialize('Product Listing', {
category: 'Electronics',
totalProducts: 150,
})
// When user applies filter
pageProps.addProperties({
activeFilters: ['brand:Apple', 'price:500-1000'],
filteredCount: 23,
})
// React component for automatic page properties
import {useEffect} from 'react'
import {useLocation, useParams} from 'react-router-dom'
function PagePropertySetter({pageName, children, getProperties}) {
const location = useLocation()
const params = useParams()
useEffect(() => {
const properties = getProperties ? getProperties(params, location) : {}
FS('setProperties', {
type: 'page',
properties: {
pageName,
...properties,
path: location.pathname,
queryParams: Object.fromEntries(new URLSearchParams(location.search)),
},
})
}, [pageName, location, params, getProperties])
return children
}
// Usage
function App() {
return (
<Routes>
<Route
path="/products/:category"
element={
<PagePropertySetter
pageName="Product Listing"
getProperties={(params) => ({
category: params.category,
})}
>
<ProductListingPage />
</PagePropertySetter>
}
/>
<Route
path="/product/:id"
element={
<PagePropertySetter
pageName="Product Detail"
getProperties={(params) => ({
productId: params.id,
})}
>
<ProductDetailPage />
</PagePropertySetter>
}
/>
</Routes>
)
}
// Complete search page property management
class SearchPageProperties {
constructor() {
this.initialized = false
}
initialize() {
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Search Results',
},
})
this.initialized = true
}
setSearchContext(query, results) {
if (!this.initialized) this.initialize()
FS('setProperties', {
type: 'page',
properties: {
searchTerm: query.term,
searchCategory: query.category,
resultsCount: results.total,
hasResults: results.total > 0,
responseTime: results.timing,
},
})
}
setFilters(filters) {
FS('setProperties', {
type: 'page',
properties: {
activeFilters: filters.active,
filterCount: filters.active.length,
priceMin: filters.price?.min,
priceMax: filters.price?.max,
brandFilters: filters.brands,
ratingFilter: filters.minRating,
},
})
}
setSorting(sort) {
FS('setProperties', {
type: 'page',
properties: {
sortField: sort.field,
sortDirection: sort.direction,
},
})
}
setPagination(page, total) {
FS('setProperties', {
type: 'page',
properties: {
currentPage: page,
totalPages: total,
},
})
}
}
// User properties: who they are
FS('setIdentity', {
uid: user.id,
properties: {
plan: 'enterprise',
role: 'admin',
},
})
// Page properties: where they are and what they're doing
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Reports Dashboard',
reportType: 'revenue',
dateRange: 'last_quarter',
},
})
// Set page context
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Product Detail',
productId: 'SKU-123',
price: 99.99,
},
})
// Events inherit page context for search
FS('trackEvent', {
name: 'Add to Cart',
properties: {
quantity: 2,
},
})
// This event is searchable via: "Add to Cart on Product Detail page"
// Page-level context
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Search Results',
searchTerm: 'laptop',
resultsCount: 50,
},
})
// Element-level context (on each product card)
// data-fs-properties-schema for product-specific data
// Elements inherit page properties automatically
Symptom: Pages don't appear in Journey builder
Common Causes:
Solutions:
Symptom: Properties disappear mid-session
Common Causes:
Solutions:
Symptom: All SPA pages have same properties
Common Causes:
Solutions:
When helping developers implement Page Properties:
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 Page Properties 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