xiaohongshu
xiaohongshu
fullstory-observe-callbacks
Comprehensive guide for implementing Fullstory's Observer/Callback API (observe method) for web applications. Teaches proper event subscription, callback handling, observer cleanup, and reacting to Fullstory lifecycle events. Includes detailed good/bad examples for session URL capture, initializa...
Full skill instructions
Fullstory's Observer API allows developers to register callbacks that react to Fullstory lifecycle events. Instead of polling or guessing when Fullstory is ready, you can subscribe to specific events and be notified when they occur. This is essential for:
| Type | Fires When | Callback Receives |
|---|---|---|
'start' | Fullstory begins capturing | undefined |
'session' | Session URL becomes available | { url: string } |
FS('observe', {...}) → Returns Observer → Callback fires when event occurs
↓
Call observer.disconnect() → Stops listening
| Behavior | Description |
|---|---|
| Immediate callback | If event already occurred, callback fires immediately |
| Multiple observers | Can register multiple observers for same event |
| Disconnect cleanup | Must disconnect to prevent memory leaks |
| Async version | Use observeAsync to wait for registration |
const observer = FS('observe', {
type: string, // Required: Event type ('start' or 'session')
callback: function // Required: Function to call when event fires
});
// Later: stop observing
observer.disconnect();
const observer = await FS('observeAsync', {
type: string,
callback: function
});
observer.disconnect();
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Event type: 'start' or 'session' |
callback | function | Yes | Function called when event occurs |
Observer object with:
disconnect(): Method to stop observing| Event Type | Callback Argument |
|---|---|
'start' | undefined |
'session' | { url: string } - Object containing session URL |
// GOOD: Forward session URL to support/analytics tools
function initializeSessionTracking() {
const observer = FS('observe', {
type: 'session',
callback: (session) => {
const sessionUrl = session.url
// Send to support tool (e.g., Intercom, Zendesk)
if (window.Intercom) {
window.Intercom('update', {
fullstory_url: sessionUrl,
})
}
// Send to error tracking (e.g., Sentry, Bugsnag)
if (window.Sentry) {
window.Sentry.setTag('fullstory_url', sessionUrl)
}
// Store for later use
window.__fullstorySessionUrl = sessionUrl
console.log('Session URL captured:', sessionUrl)
},
})
// Return cleanup function
return () => observer.disconnect()
}
// Initialize on app load
const cleanup = initializeSessionTracking()
// On app cleanup (e.g., SPA unmount)
// cleanup();
Why this is good:
// GOOD: React hook for Fullstory session management
import {useState, useEffect, useCallback} from 'react'
function useFullstorySession() {
const [sessionUrl, setSessionUrl] = useState(null)
const [isCapturing, setIsCapturing] = useState(false)
const [isReady, setIsReady] = useState(false)
useEffect(() => {
// Check if FS is available
if (typeof FS === 'undefined') {
return
}
const observers = []
// Listen for capture start
const startObserver = FS('observe', {
type: 'start',
callback: () => {
setIsCapturing(true)
setIsReady(true)
},
})
observers.push(startObserver)
// Listen for session URL
const sessionObserver = FS('observe', {
type: 'session',
callback: (session) => {
setSessionUrl(session.url)
},
})
observers.push(sessionObserver)
// Cleanup on unmount
return () => {
observers.forEach((obs) => obs.disconnect())
}
}, [])
const copySessionUrl = useCallback(() => {
if (sessionUrl) {
navigator.clipboard.writeText(sessionUrl)
return true
}
return false
}, [sessionUrl])
return {
sessionUrl,
isCapturing,
isReady,
copySessionUrl,
}
}
// Usage in component
function SupportButton() {
const {sessionUrl, isReady} = useFullstorySession()
const handleSupportClick = () => {
openSupportChat({
fullstoryUrl: sessionUrl,
})
}
return (
<button onClick={handleSupportClick} disabled={!isReady}>
Contact Support
{sessionUrl && ' (Session will be attached)'}
</button>
)
}
Why this is good:
// GOOD: Wait for Fullstory before initializing dependent features
class AnalyticsManager {
constructor() {
this.isFullstoryReady = false
this.sessionUrl = null
this.observers = []
this.pendingEvents = []
}
initialize() {
if (typeof FS === 'undefined') {
console.warn('Fullstory not available')
this.flushPendingEvents() // Send without FS context
return
}
// Wait for Fullstory to start
const startObserver = FS('observe', {
type: 'start',
callback: () => {
this.isFullstoryReady = true
this.flushPendingEvents()
},
})
this.observers.push(startObserver)
// Capture session URL
const sessionObserver = FS('observe', {
type: 'session',
callback: (session) => {
this.sessionUrl = session.url
this.updateAnalyticsContext()
},
})
this.observers.push(sessionObserver)
}
track(eventName, properties) {
const enrichedEvent = {
...properties,
fullstoryUrl: this.sessionUrl,
fullstoryReady: this.isFullstoryReady,
}
if (this.isFullstoryReady) {
this.sendToAnalytics(eventName, enrichedEvent)
} else {
// Queue until Fullstory is ready
this.pendingEvents.push({eventName, properties: enrichedEvent})
}
}
flushPendingEvents() {
this.pendingEvents.forEach((event) => {
this.sendToAnalytics(event.eventName, event.properties)
})
this.pendingEvents = []
}
updateAnalyticsContext() {
// Update context in other analytics tools
if (window.analytics) {
window.analytics.identify({
fullstoryUrl: this.sessionUrl,
})
}
}
sendToAnalytics(eventName, properties) {
// Send to your analytics platform
console.log('Analytics:', eventName, properties)
}
cleanup() {
this.observers.forEach((obs) => obs.disconnect())
this.observers = []
}
}
// Usage
const analytics = new AnalyticsManager()
analytics.initialize()
// Track events (will be queued if FS not ready)
analytics.track('Page Viewed', {page: '/home'})
Why this is good:
// GOOD: Using async observers with error handling
async function setupFullstoryIntegration() {
try {
// Set up start observer
const startObserver = await FS('observeAsync', {
type: 'start',
callback: () => {
console.log('Fullstory started capturing')
enableSessionReplayFeatures()
},
})
// Set up session observer
const sessionObserver = await FS('observeAsync', {
type: 'session',
callback: (session) => {
console.log('Session URL:', session.url)
notifyIntegrations(session.url)
},
})
// Return combined cleanup
return {
cleanup: () => {
startObserver.disconnect()
sessionObserver.disconnect()
},
status: 'success',
}
} catch (error) {
console.warn('Fullstory integration failed:', error)
return {
cleanup: () => {},
status: 'failed',
error,
}
}
}
// Usage
let fsIntegration = null
async function initApp() {
fsIntegration = await setupFullstoryIntegration()
if (fsIntegration.status === 'success') {
showSessionReplayBadge()
}
}
function cleanupApp() {
if (fsIntegration) {
fsIntegration.cleanup()
}
}
Why this is good:
// GOOD: Attach session URL to all error reports
class ErrorReporter {
constructor() {
this.sessionUrl = null
this.observer = null
}
initialize() {
// Set up session observer
this.observer = FS('observe', {
type: 'session',
callback: (session) => {
this.sessionUrl = session.url
// Update Sentry context
if (window.Sentry) {
window.Sentry.setContext('fullstory', {
sessionUrl: session.url,
})
}
// Update Bugsnag
if (window.Bugsnag) {
window.Bugsnag.addMetadata('fullstory', {
sessionUrl: session.url,
})
}
},
})
// Set up global error handler
window.addEventListener('error', (event) => {
this.reportError(event.error, {
source: 'window.onerror',
filename: event.filename,
lineno: event.lineno,
})
})
window.addEventListener('unhandledrejection', (event) => {
this.reportError(event.reason, {
source: 'unhandledrejection',
})
})
}
reportError(error, context = {}) {
const report = {
error: error?.message || String(error),
stack: error?.stack,
fullstoryUrl: this.sessionUrl,
timestamp: new Date().toISOString(),
...context,
}
// Send to your error service
this.sendErrorReport(report)
// Also log to Fullstory
if (typeof FS !== 'undefined') {
FS('log', {
level: 'error',
msg: report.error,
})
}
}
sendErrorReport(report) {
// Send to backend
fetch('/api/errors', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(report),
}).catch(console.error)
}
cleanup() {
if (this.observer) {
this.observer.disconnect()
}
}
}
// Initialize on app start
const errorReporter = new ErrorReporter()
errorReporter.initialize()
Why this is good:
// BAD: Observer never cleaned up
function setupSessionCallback() {
FS('observe', {
type: 'session',
callback: (session) => {
console.log('Session:', session.url)
},
})
// Observer never stored or cleaned up!
}
// Called multiple times in SPA
setupSessionCallback() // Leak
setupSessionCallback() // Another leak
setupSessionCallback() // More leaks...
Why this is bad:
CORRECTED VERSION:
// GOOD: Store and cleanup observer
let sessionObserver = null
function setupSessionCallback() {
// Clean up existing observer first
if (sessionObserver) {
sessionObserver.disconnect()
}
sessionObserver = FS('observe', {
type: 'session',
callback: (session) => {
console.log('Session:', session.url)
},
})
}
function cleanup() {
if (sessionObserver) {
sessionObserver.disconnect()
sessionObserver = null
}
}
// BAD: Invalid event type
FS('observe', {
type: 'ready', // BAD: Not a valid type!
callback: () => console.log('Ready!'),
})
FS('observe', {
type: 'url', // BAD: Not a valid type!
callback: (url) => console.log(url),
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Use valid event types
FS('observe', {
type: 'start', // Valid: when capture starts
callback: () => console.log('Fullstory started!'),
})
FS('observe', {
type: 'session', // Valid: when session URL ready
callback: (session) => console.log('URL:', session.url),
})
// BAD: Expecting session data from 'start' callback
FS('observe', {
type: 'start',
callback: (session) => {
// BAD: session is undefined for 'start' type!
console.log('Session URL:', session.url) // Error!
},
})
Why this is bad:
CORRECTED VERSION:
// GOOD: Use 'session' type for URL
FS('observe', {
type: 'session', // Correct type for getting URL
callback: (session) => {
console.log('Session URL:', session.url) // Works!
},
})
// BAD: Blocking app initialization on observer
async function initApp() {
let sessionUrl = null
// This will never resolve because observe doesn't return a promise
// that waits for the callback!
await new Promise((resolve) => {
FS('observe', {
type: 'session',
callback: (session) => {
sessionUrl = session.url
resolve()
},
})
})
// If Fullstory is blocked, this never runs
startApp(sessionUrl)
}
Why this is bad:
CORRECTED VERSION:
// GOOD: Non-blocking with timeout
async function initApp() {
let sessionUrl = null
// Set up observer but don't block
const sessionPromise = new Promise((resolve) => {
if (typeof FS === 'undefined') {
resolve(null)
return
}
FS('observe', {
type: 'session',
callback: (session) => {
sessionUrl = session.url
resolve(session.url)
},
})
// Timeout after 5 seconds
setTimeout(() => resolve(null), 5000)
})
// Start app immediately
startApp(null)
// Update with session URL when available
sessionPromise.then((url) => {
if (url) updateAppWithSessionUrl(url)
})
}
// BAD: Using FS before it's defined
<script>
// FS might not be defined yet!
FS('observe', {
type: 'session',
callback: (session) => {
console.log(session.url);
}
});
</script>
<script src="fullstory-snippet.js"></script>
Why this is bad:
CORRECTED VERSION:
// GOOD: Check FS exists first, or use after snippet
<script src="fullstory-snippet.js"></script>
<script>
if (typeof FS !== 'undefined') {
FS('observe', {
type: 'session',
callback: (session) => {
console.log(session.url);
}
});
}
</script>
// OR: Use DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
if (typeof FS !== 'undefined') {
FS('observe', {...});
}
});
// Centralized observer management
class FullstoryObserverManager {
constructor() {
this.observers = new Map()
}
register(name, type, callback) {
// Disconnect existing observer with same name
if (this.observers.has(name)) {
this.observers.get(name).disconnect()
}
if (typeof FS === 'undefined') {
console.warn(`Cannot register observer '${name}': FS not available`)
return false
}
const observer = FS('observe', {type, callback})
this.observers.set(name, observer)
return true
}
unregister(name) {
if (this.observers.has(name)) {
this.observers.get(name).disconnect()
this.observers.delete(name)
}
}
unregisterAll() {
this.observers.forEach((obs) => obs.disconnect())
this.observers.clear()
}
}
// Usage
const fsObservers = new FullstoryObserverManager()
fsObservers.register('session', 'session', (session) => {
console.log('Session URL:', session.url)
})
fsObservers.register('start', 'start', () => {
console.log('Fullstory started')
})
// Cleanup
fsObservers.unregisterAll()
// Vue 3 composable for Fullstory session
import { ref, onMounted, onUnmounted } from 'vue';
export function useFullstorySession() {
const sessionUrl = ref(null);
const isCapturing = ref(false);
const observers = [];
onMounted(() => {
if (typeof FS === 'undefined') return;
observers.push(
FS('observe', {
type: 'start',
callback: () => {
isCapturing.value = true;
}
})
);
observers.push(
FS('observe', {
type: 'session',
callback: (session) => {
sessionUrl.value = session.url;
}
})
);
});
onUnmounted(() => {
observers.forEach(obs => obs.disconnect());
});
return {
sessionUrl,
isCapturing
};
}
// Usage in component
<script setup>
import { useFullstorySession } from './useFullstorySession';
const { sessionUrl, isCapturing } = useFullstorySession();
</script>
<template>
<div v-if="isCapturing">
Session is being recorded
<a v-if="sessionUrl" :href="sessionUrl" target="_blank">
View Session
</a>
</div>
</template>
// Event emitter for Fullstory events
class FullstoryEventEmitter {
constructor() {
this.listeners = {
start: [],
session: [],
}
this.state = {
started: false,
sessionUrl: null,
}
this.observers = []
this.initialize()
}
initialize() {
if (typeof FS === 'undefined') return
this.observers.push(
FS('observe', {
type: 'start',
callback: () => {
this.state.started = true
this.emit('start')
},
}),
)
this.observers.push(
FS('observe', {
type: 'session',
callback: (session) => {
this.state.sessionUrl = session.url
this.emit('session', session)
},
}),
)
}
on(event, callback) {
this.listeners[event]?.push(callback)
// If event already occurred, call immediately
if (event === 'start' && this.state.started) {
callback()
}
if (event === 'session' && this.state.sessionUrl) {
callback({url: this.state.sessionUrl})
}
return () => this.off(event, callback)
}
off(event, callback) {
const idx = this.listeners[event]?.indexOf(callback)
if (idx > -1) {
this.listeners[event].splice(idx, 1)
}
}
emit(event, data) {
this.listeners[event]?.forEach((cb) => cb(data))
}
destroy() {
this.observers.forEach((obs) => obs.disconnect())
this.listeners = {start: [], session: []}
}
}
// Global instance
const fsEvents = new FullstoryEventEmitter()
// Usage
const unsubscribe = fsEvents.on('session', (session) => {
console.log('Session URL:', session.url)
})
// Later
unsubscribe()
Symptom: Observer registered but callback never called
Common Causes:
Solutions:
Symptom: Callback fires multiple times unexpectedly
Common Causes:
Solutions:
Symptom: Memory usage grows over time
Common Causes:
Solutions:
When helping developers with Observer API:
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 Observer/Callback 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