tessl install github:jeremylongshore/claude-code-plugins-plus-skills --skill customerio-known-pitfallsgithub.com/jeremylongshore/claude-code-plugins-plus-skills
Identify and avoid Customer.io anti-patterns. Use when reviewing integrations, avoiding common mistakes, or optimizing existing Customer.io implementations. Trigger with phrases like "customer.io mistakes", "customer.io anti-patterns", "customer.io best practices", "customer.io gotchas".
Review Score
88%
Validation Score
12/16
Implementation Score
88%
Activation Score
90%
Avoid common mistakes and anti-patterns when integrating with Customer.io.
// WRONG: Using App API key for tracking
const client = new TrackClient(siteId, appApiKey); // Will fail!
// CORRECT: Use Track API key for tracking
const client = new TrackClient(siteId, trackApiKey);
// Use App API key only for transactional and reporting APIs
const apiClient = new APIClient(appApiKey);// WRONG: JavaScript milliseconds
{ created_at: Date.now() } // 1704067200000 - will be rejected!
// CORRECT: Unix seconds
{ created_at: Math.floor(Date.now() / 1000) } // 1704067200// WRONG: Credentials in code
const client = new TrackClient('abc123', 'secret-key'); // Security risk!
// CORRECT: Environment variables
const client = new TrackClient(
process.env.CUSTOMERIO_SITE_ID!,
process.env.CUSTOMERIO_API_KEY!
);// WRONG: Track before identify
await client.track(userId, { name: 'signup' }); // User doesn't exist!
await client.identify(userId, { email: 'user@example.com' });
// CORRECT: Always identify first
await client.identify(userId, { email: 'user@example.com' });
await client.track(userId, { name: 'signup' });// WRONG: User ID changes when email changes
const userId = user.email; // Changing email = new user!
// CORRECT: Use immutable identifier
const userId = user.databaseId; // UUIDs or auto-increment IDs// WRONG: No anonymous_id linking
await client.identify(newUserId, { email: 'user@example.com' });
// Anonymous activity is orphaned!
// CORRECT: Include anonymous_id for merging
await client.identify(newUserId, {
email: 'user@example.com',
anonymous_id: previousAnonymousId
});// WRONG: Inconsistent casing and naming
await client.track(userId, { name: 'UserSignedUp' });
await client.track(userId, { name: 'user-signed-up' });
await client.track(userId, { name: 'user_signedup' });
// CORRECT: Consistent snake_case
await client.track(userId, { name: 'user_signed_up' });// WRONG: Dynamic event names create clutter
await client.track(userId, { name: `viewed_product_${productId}` });
// Creates thousands of unique events!
// CORRECT: Use properties for variations
await client.track(userId, {
name: 'product_viewed',
data: { product_id: productId }
});// WRONG: Waiting for analytics in request path
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await client.identify(user.id, { email: user.email }); // Blocks!
res.json({ user });
});
// CORRECT: Fire-and-forget
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
client.identify(user.id, { email: user.email })
.catch(err => console.error('Customer.io error:', err));
res.json({ user });
});// WRONG: No email attribute
await client.identify(userId, { name: 'John' });
// User can't receive emails!
// CORRECT: Always include email for email campaigns
await client.identify(userId, {
email: 'john@example.com',
name: 'John'
});// WRONG: Sometimes string, sometimes number
await client.identify(userId1, { plan: 'premium' });
await client.identify(userId2, { plan: 1 });
// CORRECT: Consistent types
await client.identify(userId, { plan: 'premium' });// WRONG: PII exposed
await client.track(userId, { name: `email_${user.email}` });
// Creates segment: "email_john@example.com"
// CORRECT: Use attributes, not names
await client.track(userId, {
name: 'email_action',
data: { email: user.email }
});## WRONG: No unsubscribe link
Email template without {{{ unsubscribe_url }}}
## CORRECT: Always include unsubscribe
<a href="{{{ unsubscribe_url }}}">Unsubscribe</a># WRONG: Trigger fires on every identify
trigger:
event: "identify"
# CORRECT: Trigger on specific events
trigger:
event: "signed_up"// WRONG: No bounce handling
webhooks.on('email_bounced', () => {
// Do nothing
});
// CORRECT: Suppress or update on bounce
webhooks.on('email_bounced', async (event) => {
await client.suppress(event.data.customer_id);
// Or mark email as invalid in your database
});// WRONG: Ignoring spam complaints
// Leads to deliverability issues!
// CORRECT: Alert on complaints
webhooks.on('email_complained', async (event) => {
// Immediately suppress
await client.suppress(event.data.customer_id);
// Alert the team
await alertTeam(`Spam complaint from ${event.data.email_address}`);
});// WRONG: New client per request
app.get('/api', async (req, res) => {
const client = new TrackClient(siteId, apiKey); // Creates new connection!
await client.identify(userId, data);
});
// CORRECT: Reuse client
const client = new TrackClient(siteId, apiKey);
app.get('/api', async (req, res) => {
await client.identify(userId, data);
});// WRONG: Uncontrolled burst
for (const user of users) {
await client.identify(user.id, user.data); // 10k requests instantly!
}
// CORRECT: Rate limited
const limiter = new Bottleneck({ maxConcurrent: 10, minTime: 10 });
for (const user of users) {
await limiter.schedule(() => client.identify(user.id, user.data));
}// scripts/audit-integration.ts
interface AuditResult {
issues: string[];
warnings: string[];
score: number;
}
async function auditIntegration(): Promise<AuditResult> {
const result: AuditResult = { issues: [], warnings: [], score: 100 };
// Check for hardcoded credentials
const files = await glob('**/*.{ts,js}');
for (const file of files) {
const content = await readFile(file, 'utf-8');
if (content.includes('site_') && content.includes('api_')) {
result.issues.push(`Possible hardcoded credentials in ${file}`);
result.score -= 20;
}
}
// Check for millisecond timestamps
if (await hasPattern(/Date\.now\(\)(?!\s*\/\s*1000)/)) {
result.warnings.push('Possible millisecond timestamps detected');
result.score -= 5;
}
// Check for track before identify pattern
if (await hasPattern(/track\([^)]+\)[\s\S]{0,500}identify\(/)) {
result.issues.push('Track before identify pattern detected');
result.score -= 15;
}
return result;
}| Pitfall | Fix |
|---|---|
| Wrong API key | Track API for tracking, App API for transactional |
| Milliseconds | Use Math.floor(Date.now() / 1000) |
| Track before identify | Always identify first |
| Changing user IDs | Use immutable database IDs |
| No email attribute | Include email for email campaigns |
| Dynamic event names | Use properties instead |
| Blocking requests | Fire-and-forget pattern |
| No bounce handling | Suppress on bounce |
| No rate limiting | Use Bottleneck or similar |
Following these guidelines will help you avoid common pitfalls and build a reliable Customer.io integration. Regularly audit your implementation against this checklist to catch issues early.