476 lines
9.2 KiB
Plaintext
476 lines
9.2 KiB
Plaintext
---
|
|
title: Event Tracking Integration
|
|
description: Track user behavior to trigger workflows
|
|
icon: Activity
|
|
---
|
|
|
|
## Overview
|
|
|
|
Track events from your application to trigger workflows and update contact data. Events like `user_signed_up`, `purchase_completed`, `feature_used` can start automated email sequences.
|
|
|
|
## Get your public key
|
|
|
|
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
|
|
2. Copy your **Public Key** (starts with `pk_`)
|
|
|
|
Public keys are safe to use in client-side code.
|
|
|
|
## Basic event tracking
|
|
|
|
### JavaScript (client-side)
|
|
|
|
```javascript
|
|
await fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
event: 'button_clicked',
|
|
email: user.email,
|
|
data: {
|
|
buttonName: 'Get Started',
|
|
page: '/pricing'
|
|
}
|
|
})
|
|
});
|
|
```
|
|
|
|
### Node.js (server-side)
|
|
|
|
```javascript
|
|
await fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${process.env.PLUNK_PUBLIC_KEY}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
event: 'user_signed_up',
|
|
email: user.email,
|
|
data: {
|
|
name: user.name,
|
|
plan: 'free',
|
|
signupDate: new Date().toISOString()
|
|
}
|
|
})
|
|
});
|
|
```
|
|
|
|
### Python
|
|
|
|
```python
|
|
import requests
|
|
import os
|
|
|
|
requests.post('{{API_URL}}/v1/track',
|
|
headers={
|
|
'Authorization': f'Bearer {os.environ["PLUNK_PUBLIC_KEY"]}',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
json={
|
|
'event': 'purchase_completed',
|
|
'email': user.email,
|
|
'data': {
|
|
'orderId': order.id,
|
|
'total': order.total,
|
|
'items': order.items
|
|
}
|
|
}
|
|
)
|
|
```
|
|
|
|
## Common events to track
|
|
|
|
### User lifecycle
|
|
|
|
```javascript
|
|
// Signup
|
|
await trackEvent('user_signed_up', user.email, {
|
|
name: user.name,
|
|
source: 'google',
|
|
plan: 'free'
|
|
});
|
|
|
|
// Activation
|
|
await trackEvent('first_value_achieved', user.email, {
|
|
action: 'created_first_project',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Upgrade
|
|
await trackEvent('subscription_upgraded', user.email, {
|
|
fromPlan: 'free',
|
|
toPlan: 'premium',
|
|
mrr: 99
|
|
});
|
|
|
|
// Churn
|
|
await trackEvent('subscription_cancelled', user.email, {
|
|
reason: user.cancellationReason,
|
|
cancelledAt: new Date().toISOString()
|
|
});
|
|
```
|
|
|
|
### Product engagement
|
|
|
|
```javascript
|
|
// Feature usage
|
|
await trackEvent('feature_used', user.email, {
|
|
featureName: 'data_export',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Content interaction
|
|
await trackEvent('video_watched', user.email, {
|
|
videoId: 'intro-101',
|
|
duration: 300,
|
|
completed: true
|
|
});
|
|
|
|
// Settings changes
|
|
await trackEvent('settings_updated', user.email, {
|
|
setting: 'notifications',
|
|
value: 'enabled'
|
|
});
|
|
```
|
|
|
|
### E-commerce
|
|
|
|
```javascript
|
|
// Cart
|
|
await trackEvent('cart_abandoned', user.email, {
|
|
cartId: cart.id,
|
|
cartTotal: cart.total,
|
|
items: cart.items.map(i => i.name)
|
|
});
|
|
|
|
// Purchase
|
|
await trackEvent('purchase_completed', user.email, {
|
|
orderId: order.id,
|
|
total: order.total,
|
|
paymentMethod: 'credit_card'
|
|
});
|
|
|
|
// Review
|
|
await trackEvent('review_submitted', user.email, {
|
|
productId: product.id,
|
|
rating: 5
|
|
});
|
|
```
|
|
|
|
## Event naming conventions
|
|
|
|
**Use lowercase with underscores:**
|
|
- ✅ `user_signed_up`
|
|
- ✅ `purchase_completed`
|
|
- ❌ `UserSignedUp`
|
|
- ❌ `purchase-completed`
|
|
|
|
**Be specific:**
|
|
- ✅ `trial_started`
|
|
- ❌ `event`
|
|
|
|
**Use past tense:**
|
|
- ✅ `email_opened`
|
|
- ❌ `email_open`
|
|
|
|
## Event data best practices
|
|
|
|
**Keep data flat when possible:**
|
|
|
|
```javascript
|
|
// Good
|
|
{
|
|
name: 'John',
|
|
plan: 'premium',
|
|
mrr: 99
|
|
}
|
|
|
|
// Works but harder to use
|
|
{
|
|
user: {
|
|
profile: {
|
|
name: 'John'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Use consistent types:**
|
|
|
|
```javascript
|
|
// Good - number for numeric values
|
|
{ total: 99.99 }
|
|
|
|
// Bad - string for numeric values
|
|
{ total: "99.99" }
|
|
```
|
|
|
|
**Use ISO dates:**
|
|
|
|
```javascript
|
|
// Good
|
|
{ signupDate: new Date().toISOString() }
|
|
|
|
// Okay but less flexible
|
|
{ signupDate: '2024-03-15' }
|
|
```
|
|
|
|
## Integrate with React
|
|
|
|
### Context provider
|
|
|
|
```javascript
|
|
// EventTrackingContext.js
|
|
import { createContext, useContext } from 'react';
|
|
|
|
const EventTrackingContext = createContext();
|
|
|
|
export function EventTrackingProvider({ children }) {
|
|
const trackEvent = async (event, data = {}) => {
|
|
const user = getCurrentUser(); // Your auth logic
|
|
|
|
if (!user?.email) return;
|
|
|
|
await fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
event,
|
|
email: user.email,
|
|
data: {
|
|
name: user.name,
|
|
...data
|
|
}
|
|
})
|
|
});
|
|
};
|
|
|
|
return (
|
|
<EventTrackingContext.Provider value={{ trackEvent }}>
|
|
{children}
|
|
</EventTrackingContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useEventTracking = () => useContext(EventTrackingContext);
|
|
```
|
|
|
|
### Use in components
|
|
|
|
```javascript
|
|
import { useEventTracking } from './EventTrackingContext';
|
|
|
|
function UpgradeButton() {
|
|
const { trackEvent } = useEventTracking();
|
|
|
|
const handleUpgrade = async () => {
|
|
await upgradePlan('premium');
|
|
|
|
await trackEvent('plan_upgraded', {
|
|
plan: 'premium',
|
|
source: 'pricing_page'
|
|
});
|
|
};
|
|
|
|
return <button onClick={handleUpgrade}>Upgrade</button>;
|
|
}
|
|
```
|
|
|
|
## Integrate with Next.js
|
|
|
|
### Client component
|
|
|
|
```javascript
|
|
'use client';
|
|
|
|
import { trackEvent } from '@/lib/plunk';
|
|
|
|
export function SignupForm() {
|
|
const handleSubmit = async (data) => {
|
|
const user = await createUser(data);
|
|
|
|
// Track event
|
|
await trackEvent('user_signed_up', user.email, {
|
|
name: user.name,
|
|
source: 'homepage'
|
|
});
|
|
};
|
|
|
|
return <form onSubmit={handleSubmit}>...</form>;
|
|
}
|
|
```
|
|
|
|
### Server action
|
|
|
|
```javascript
|
|
'use server';
|
|
|
|
import { trackEvent } from '@/lib/plunk';
|
|
|
|
export async function createProject(formData) {
|
|
const user = await getCurrentUser();
|
|
const project = await db.projects.create({
|
|
name: formData.get('name'),
|
|
userId: user.id
|
|
});
|
|
|
|
await trackEvent('project_created', user.email, {
|
|
projectId: project.id,
|
|
projectName: project.name
|
|
});
|
|
|
|
return project;
|
|
}
|
|
```
|
|
|
|
## Create a helper function
|
|
|
|
```javascript
|
|
// lib/plunk.js
|
|
const PLUNK_PUBLIC_KEY = process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY;
|
|
|
|
export async function trackEvent(event, email, data = {}) {
|
|
try {
|
|
const response = await fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
event,
|
|
email,
|
|
data
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Failed to track event:', error);
|
|
// Don't throw - tracking shouldn't break your app
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing events
|
|
|
|
### View tracked events
|
|
|
|
1. Go to **Activity** in Plunk dashboard
|
|
2. Filter by event type
|
|
3. View event data payloads
|
|
|
|
### Test locally
|
|
|
|
```javascript
|
|
// Track a test event
|
|
await fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer pk_your_key',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
event: 'test_event',
|
|
email: '[email protected]',
|
|
data: {
|
|
test: true,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
})
|
|
});
|
|
```
|
|
|
|
Check Plunk Activity - event should appear within seconds.
|
|
|
|
## Connect to workflows
|
|
|
|
Once events are tracked, create workflows that trigger on them:
|
|
|
|
1. **Workflows** → **Create Workflow**
|
|
2. Trigger: Event `user_signed_up`
|
|
3. Build your automation
|
|
4. Enable workflow
|
|
|
|
Now when you track `user_signed_up`, the workflow runs automatically.
|
|
|
|
## Performance considerations
|
|
|
|
**Don't block user actions:**
|
|
|
|
```javascript
|
|
// Good - fire and forget
|
|
handleClick() {
|
|
trackEvent('button_clicked', user.email);
|
|
// Don't await
|
|
}
|
|
|
|
// Bad - user waits for tracking
|
|
async handleClick() {
|
|
await trackEvent('button_clicked', user.email);
|
|
// User has to wait
|
|
}
|
|
```
|
|
|
|
**Batch events for bulk operations:**
|
|
|
|
```javascript
|
|
// If importing 1000 users, track events in background
|
|
async function importUsers(users) {
|
|
const imported = await db.users.bulkCreate(users);
|
|
|
|
// Queue for background processing
|
|
await queue.add('track-events', {
|
|
event: 'user_imported',
|
|
users: imported
|
|
});
|
|
}
|
|
```
|
|
|
|
**Add retry logic:**
|
|
|
|
```javascript
|
|
async function trackEventWithRetry(event, email, data, retries = 3) {
|
|
for (let i = 0; i < retries; i++) {
|
|
try {
|
|
return await trackEvent(event, email, data);
|
|
} catch (error) {
|
|
if (i === retries - 1) throw error;
|
|
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common issues
|
|
|
|
**Event tracked but workflow not triggering**
|
|
- Workflow is enabled
|
|
- Event name matches exactly (case-sensitive)
|
|
- Contact exists in Plunk
|
|
- Contact is subscribed
|
|
|
|
**CORS errors in browser**
|
|
- Use public key (not secret key)
|
|
- Plunk API allows CORS from all origins
|
|
|
|
**Contact not created**
|
|
- Email must be valid
|
|
- Contact is created automatically when event is tracked
|
|
|
|
## Next steps
|
|
|
|
- [Build a workflow](/tutorials/welcome-series-workflow) triggered by events
|
|
- [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) decision guide
|
|
- [Stripe integration](/integrations/stripe-billing) for billing events
|