Tracking Approaches
Pendo offers both client-side and server-side tracking approaches, each designed for specific use cases. Understanding the differences and when to use each method will help you implement Pendo effectively.
Client-Side Tracking
Client-side tracking is Pendo's primary implementation method, using JavaScript that runs directly in the user's browser.
What Client-Side Tracks
Automatic Tracking
- Page views and navigation
- Feature clicks and interactions
- Form submissions
- Session duration and user activity
- Element visibility and engagement
- Browser and device information
Manual Tracking
// Custom events
pendo.track('Feature Used', {
feature_name: 'Advanced Search',
user_action: 'filter_applied'
});
// User identification
pendo.identify({
visitor: {
id: 'user-123',
email: 'user@example.com'
}
});
Client-Side Capabilities
In-App Guides and Messages
One of Pendo's most powerful features, only available client-side:
// Guides automatically appear based on rules set in Pendo UI
// No additional code needed beyond initialization
// Programmatically control guides
pendo.showGuideById('guide-id');
pendo.dismissActiveGuides();
Visual Design and Tagging
The Pendo Visual Designer allows non-technical users to:
// Make elements easier to tag
<button data-pendo="checkout-button">
Checkout
</button>
Session Replay
Pendo captures user sessions for replay and analysis:
// Session replay happens automatically when enabled
// Configure in Pendo settings
// Control recording programmatically
pendo.stopSendingEvents(); // Pause recording
pendo.startSendingEvents(); // Resume recording
Client-Side Implementation
Basic Setup
<script>
(function(apiKey){
(function(p,e,n,d,o){var v,w,x,y,z;o=p[d]=p[d]||{};o._q=o._q||[];
v=['initialize','identify','updateOptions','pageLoad','track'];for(w=0,x=v.length;w<x;++w)(function(m){
o[m]=o[m]||function(){o._q[m===v[0]?'unshift':'push']([m].concat([].slice.call(arguments,0)));};})(v[w]);
y=e.createElement(n);y.async=!0;y.src='https://cdn.pendo.io/agent/static/'+apiKey+'/pendo.js';
z=e.getElementsByTagName(n)[0];z.parentNode.insertBefore(y,z);})(window,document,'script','pendo');
})('YOUR-API-KEY');
</script>
Full Initialization
pendo.initialize({
visitor: {
id: 'USER_ID',
email: 'user@example.com',
full_name: 'John Doe',
role: 'admin'
},
account: {
id: 'ACCOUNT_ID',
name: 'Acme Corp',
plan: 'enterprise'
}
});
Client-Side Advantages
- Rich user experience: In-app guides, tooltips, and walkthroughs
- Visual features: Session replay, heatmaps, and click tracking
- No backend changes: Quick to implement without server modifications
- Real-time feedback: Immediate user engagement and feedback collection
- Contextual help: Display guides based on user behavior and page context
Client-Side Limitations
- Browser dependency: Requires JavaScript enabled
- Ad blockers: May be blocked by privacy extensions
- Page load dependency: Can't track events before page loads
- Client-side only data: Limited to browser-accessible information
- Performance impact: Minimal but adds to page load size
Server-Side Integration
While Pendo is primarily client-side, it offers server-side capabilities through its API for specific use cases.
When to Use Server-Side
Backend Events
// Node.js example
const axios = require('axios');
async function sendPendoEvent(visitorId, accountId, eventName, properties) {
await axios.post('https://app.pendo.io/data/track', {
type: 'track',
event: eventName,
visitorId: visitorId,
accountId: accountId,
properties: properties,
timestamp: Date.now()
}, {
headers: {
'X-Pendo-Integration-Key': 'YOUR-INTEGRATION-KEY',
'Content-Type': 'application/json'
}
});
}
// Track server-side event
await sendPendoEvent(
'user-123',
'account-456',
'Payment Processed',
{
amount: 99.99,
currency: 'USD',
payment_method: 'credit_card'
}
);
Data Synchronization
// Sync user data from your database to Pendo
async function syncUserToPendo(user) {
await axios.post('https://app.pendo.io/api/v1/metadata/visitor/value', {
values: [{
visitorId: user.id,
values: {
email: user.email,
role: user.role,
created_at: user.createdAt,
last_login: user.lastLogin,
total_purchases: user.totalPurchases
}
}]
}, {
headers: {
'X-Pendo-Integration-Key': 'YOUR-INTEGRATION-KEY',
'Content-Type': 'application/json'
}
});
}
Pendo Integration API
The Integration API allows server-side data management:
Visitor Metadata
# Python example
import requests
def update_visitor_metadata(visitor_id, metadata):
url = 'https://app.pendo.io/api/v1/metadata/visitor/value'
headers = {
'X-Pendo-Integration-Key': 'YOUR-INTEGRATION-KEY',
'Content-Type': 'application/json'
}
payload = {
'values': [{
'visitorId': visitor_id,
'values': metadata
}]
}
response = requests.post(url, json=payload, headers=headers)
return response.json()
# Update user role after server-side change
update_visitor_metadata('user-123', {
'role': 'super_admin',
'permissions': ['admin', 'billing', 'analytics']
})
Account Metadata
def update_account_metadata(account_id, metadata):
url = 'https://app.pendo.io/api/v1/metadata/account/value'
headers = {
'X-Pendo-Integration-Key': 'YOUR-INTEGRATION-KEY',
'Content-Type': 'application/json'
}
payload = {
'values': [{
'accountId': account_id,
'values': metadata
}]
}
response = requests.post(url, json=payload, headers=headers)
return response.json()
# Update account after subscription change
update_account_metadata('account-456', {
'plan': 'enterprise',
'mrr': 999,
'users_count': 50
})
Server-Side Use Cases
E-commerce Events
// Track purchase completion server-side
app.post('/api/purchase/complete', async (req, res) => {
const { userId, accountId, order } = req.body;
// Process payment
const payment = await processPayment(order);
// Send to Pendo
await sendPendoEvent(userId, accountId, 'Purchase Completed', {
order_id: order.id,
revenue: order.total,
items: order.items.length,
payment_status: payment.status
});
res.json({ success: true });
});
Batch Data Updates
// Nightly sync of user data
async function syncAllUsersToPendo() {
const users = await db.users.findAll();
for (const user of users) {
await syncUserToPendo(user);
await sleep(100); // Rate limiting
}
}
// Run as cron job
cron.schedule('0 2 * * *', syncAllUsersToPendo);
Webhook Processing
// Handle webhook from payment provider
app.post('/webhooks/stripe', async (req, res) => {
const event = req.body;
if (event.type === 'subscription.updated') {
const subscription = event.data.object;
await update_account_metadata(subscription.metadata.account_id, {
subscription_status: subscription.status,
plan: subscription.plan.id,
mrr: subscription.plan.amount / 100
});
}
res.json({ received: true });
});
Hybrid Approach
Most Pendo implementations use both client-side and server-side tracking:
Division of Responsibilities
Client-Side Handles:
- User interface interactions
- Page views and navigation
- In-app guides and messages
- Session replay
- Real-time user engagement
Server-Side Handles:
- Backend events (payments, API calls)
- Data synchronization
- Batch updates
- Sensitive operations
- System-to-system integrations
Example Hybrid Implementation
// Client-side: Track UI interaction
document.getElementById('upgrade-button').addEventListener('click', () => {
pendo.track('Upgrade Button Clicked', {
current_plan: user.plan,
clicked_plan: 'enterprise'
});
// Initiate upgrade
upgradeSubscription('enterprise');
});
// Server-side: Track actual upgrade
app.post('/api/subscription/upgrade', async (req, res) => {
const { userId, accountId, newPlan } = req.body;
// Process upgrade
const result = await processUpgrade(userId, newPlan);
// Send server-side event to Pendo
await sendPendoEvent(userId, accountId, 'Subscription Upgraded', {
from_plan: user.currentPlan,
to_plan: newPlan,
upgrade_success: result.success,
new_mrr: result.newMRR
});
// Update account metadata
await update_account_metadata(accountId, {
plan: newPlan,
mrr: result.newMRR
});
res.json(result);
});
Best Practices
Client-Side Best Practices
- Initialize early: Load Pendo as early as possible in page load
- Error handling: Wrap Pendo calls in try-catch blocks
- Privacy: Respect user privacy settings and consent
- Performance: Use async loading to avoid blocking page render
// Safe initialization
try {
if (window.pendo && userConsent) {
pendo.initialize(config);
}
} catch (error) {
console.error('Pendo initialization failed:', error);
}
Server-Side Best Practices
- Rate limiting: Respect API rate limits (typically 1000 requests/minute)
- Error handling: Handle API failures gracefully
- Batching: Batch updates when possible
- Security: Keep integration keys secure
// Rate-limited batch update
const queue = new RateLimitedQueue(100); // 100 requests/minute
async function batchUpdateUsers(users) {
for (const user of users) {
await queue.add(() => syncUserToPendo(user));
}
}
Comparison Table
| Feature | Client-Side | Server-Side |
|---|---|---|
| In-app guides | Yes | No |
| Session replay | Yes | No |
| Visual tagging | Yes | No |
| UI event tracking | Yes | No |
| Backend events | Limited | Yes |
| Data sync | No | Yes |
| Batch operations | No | Yes |
| Sensitive data | Avoid | Yes |
| Real-time | Yes | Yes |
| Ad blocker resistant | No | Yes |