customerio-known-pitfalls by jeremylongshore
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".
Content & Writing
1.9K Stars
265 Forks
Updated Apr 3, 2026, 03:47 AM
Why Use This
This skill provides specialized capabilities for jeremylongshore's codebase.
Use Cases
- Developing new features in the jeremylongshore repository
- Refactoring existing code to follow jeremylongshore standards
- Understanding and working with jeremylongshore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Repository claude-code-plugins-plus-skills
Skill Version
main
Community
1.9K 265
Updated At Apr 3, 2026, 03:47 AM
Skill Stats
SKILL.md 274 Lines
Total Files 2
Total Size 9.4 KB
License MIT
--- name: customerio-known-pitfalls description: | Identify and avoid Customer.io anti-patterns and gotchas. Use when reviewing integrations, onboarding developers, or auditing existing Customer.io code. Trigger: "customer.io mistakes", "customer.io anti-patterns", "customer.io gotchas", "customer.io pitfalls", "customer.io code review". allowed-tools: Read, Write, Edit, Bash(npm:*), Bash(npx:*), Glob, Grep version: 1.0.0 license: MIT author: Jeremy Longshore <[email protected]> compatible-with: claude-code, codex, openclaw tags: [saas, customer-io, best-practices, anti-patterns] --- # Customer.io Known Pitfalls ## Overview The 12 most common Customer.io integration mistakes, with the wrong pattern, the correct pattern, and why it matters. Use this as a code review checklist and developer onboarding reference. ## The Pitfall Catalog ### Pitfall 1: Wrong API Key Type ```typescript // WRONG — using Track API key for transactional messages const api = new APIClient(process.env.CUSTOMERIO_TRACK_API_KEY!); // Gets 401 because App API uses a DIFFERENT bearer token // CORRECT — use the App API key const api = new APIClient(process.env.CUSTOMERIO_APP_API_KEY!); ``` **Why:** Customer.io has two separate authentication systems. Track API uses Basic Auth (Site ID + Track Key). App API uses Bearer Auth (App Key). They are not interchangeable. ### Pitfall 2: Millisecond Timestamps ```typescript // WRONG — JavaScript Date.now() returns milliseconds await cio.identify("user-1", { created_at: Date.now(), // 1704067200000 → year 55976 }); // CORRECT — Customer.io expects Unix seconds await cio.identify("user-1", { created_at: Math.floor(Date.now() / 1000), // 1704067200 }); ``` **Why:** Customer.io accepts millisecond values without error but interprets them as seconds, resulting in dates thousands of years in the future. Segments using date comparisons silently break. ### Pitfall 3: Track Before Identify ```typescript // WRONG — tracking before identifying creates orphaned events await cio.track("new-user", { name: "signed_up", data: {} }); // User profile doesn't exist yet — event may be lost // CORRECT — always identify first await cio.identify("new-user", { email: "[email protected]" }); await cio.track("new-user", { name: "signed_up", data: {} }); ``` **Why:** Track calls on non-existent users may be silently dropped. Always `identify()` before `track()`. ### Pitfall 4: Using Email as User ID ```typescript // WRONG — email can change, creating duplicate profiles await cio.identify("[email protected]", { email: "[email protected]" }); // When user changes email, old profile orphaned, new one created // CORRECT — use immutable database ID await cio.identify("usr_abc123", { email: "[email protected]", // Email as attribute, not ID }); ``` **Why:** The first argument to `identify()` is the permanent user ID. If you use email and the user changes it, you get two profiles. Use your database primary key instead. ### Pitfall 5: Missing Email Attribute ```typescript // WRONG — user can't receive email campaigns await cio.identify("user-1", { first_name: "Jane", plan: "pro", // No email attribute! }); // CORRECT — always include email for email campaigns await cio.identify("user-1", { email: "[email protected]", first_name: "Jane", plan: "pro", }); ``` **Why:** Without an `email` attribute, the user profile exists but can't receive any email campaigns or transactional messages. ### Pitfall 6: Dynamic Event Names ```typescript // WRONG — creates hundreds of unique event names await cio.track("user-1", { name: `viewed_${productId}`, // "viewed_SKU-12345" data: {}, }); // CORRECT — use a static name with data properties await cio.track("user-1", { name: "product_viewed", // Consistent, filterable data: { product_id: productId }, // Dynamic data in properties }); ``` **Why:** Dynamic event names pollute your event catalog and make it impossible to create campaign triggers. Use a fixed set of event names and pass variations as data properties. ### Pitfall 7: Blocking Request Path ```typescript // WRONG — API call adds 200ms+ to every request app.post("/api/action", async (req, res) => { const result = await doBusinessLogic(req.body); await cio.track(req.user.id, { name: "action_taken", data: {} }); // BLOCKS response res.json(result); }); // CORRECT — fire-and-forget for non-critical tracking app.post("/api/action", async (req, res) => { const result = await doBusinessLogic(req.body); cio.track(req.user.id, { name: "action_taken", data: {} }) .catch((err) => console.error("CIO track failed:", err.message)); res.json(result); // Returns immediately }); ``` **Why:** Customer.io tracking is non-critical analytics. Don't add 200ms+ latency to every user request. ### Pitfall 8: No Bounce Handling ```typescript // WRONG — keep sending to bounced addresses, damaging sender reputation // (No webhook handler for bounces) // CORRECT — suppress users who bounce async function handleBounceWebhook(event: { customer_id: string }) { await cio.suppress(event.customer_id); console.warn(`Suppressed bounced user: ${event.customer_id}`); } ``` **Why:** Continuing to send to bounced addresses damages your sender reputation, eventually causing all your emails to go to spam. ### Pitfall 9: New Client Per Request ```typescript // WRONG — creates a new TCP connection for every API call app.post("/track", async (req, res) => { const cio = new TrackClient(siteId, apiKey, { region: RegionUS }); await cio.track(req.body.userId, req.body.event); res.sendStatus(200); }); // CORRECT — singleton, created once const cio = new TrackClient(siteId, apiKey, { region: RegionUS }); app.post("/track", async (req, res) => { await cio.track(req.body.userId, req.body.event); res.sendStatus(200); }); ``` **Why:** Each `new TrackClient()` creates fresh connections. Singleton reuses TCP connections, reducing latency by 50-80%. ### Pitfall 10: Inconsistent Event Names ```typescript // WRONG — multiple naming conventions await cio.track(userId, { name: "UserSignedUp" }); // PascalCase await cio.track(userId, { name: "user-signed-up" }); // kebab-case await cio.track(userId, { name: "user signed up" }); // spaces // CORRECT — consistent snake_case everywhere await cio.track(userId, { name: "user_signed_up" }); ``` **Why:** Event names are case-sensitive. Inconsistent naming means campaign triggers only match one variant, and your event catalog becomes a mess. ### Pitfall 11: No Rate Limiting ```typescript // WRONG — blasting API at full speed during import for (const user of allUsers) { await cio.identify(user.id, user.attrs); // 1000+ req/sec → 429 errors } // CORRECT — rate-limited processing import Bottleneck from "bottleneck"; const limiter = new Bottleneck({ maxConcurrent: 10, minTime: 15 }); for (const user of allUsers) { await limiter.schedule(() => cio.identify(user.id, user.attrs)); } ``` **Why:** Customer.io rate limits at ~100 req/sec. Without throttling, bulk operations trigger 429 errors and potentially get your API key temporarily blocked. ### Pitfall 12: PII in Event Names ```typescript // WRONG — PII in event names is unsanitizable await cio.track(userId, { name: `[email protected]` }); // CORRECT — PII only in data properties (can be deleted per GDPR) await cio.track(userId, { name: "email_sent", data: { recipient: "[email protected]" }, }); ``` **Why:** Event names are indexed and cached. PII in event names can't be deleted for GDPR compliance. Always put PII in event `data` properties. ## Quick Reference | # | Pitfall | Fix | |---|---------|-----| | 1 | Wrong API key type | Track key for tracking, App key for transactional | | 2 | Millisecond timestamps | `Math.floor(Date.now() / 1000)` | | 3 | Track before identify | Always `identify()` first | | 4 | Email as user ID | Use immutable database ID | | 5 | Missing email attribute | Include `email` in `identify()` | | 6 | Dynamic event names | Static names, dynamic data properties | | 7 | Blocking request path | Fire-and-forget with `.catch()` | | 8 | No bounce handling | Suppress bounced users via webhook | | 9 | New client per request | Singleton pattern | | 10 | Inconsistent event names | Always `snake_case` | | 11 | No rate limiting | Use Bottleneck or p-queue | | 12 | PII in event names | PII in `data` properties only | ## Integration Audit Script ```bash # Quick grep audit for common pitfalls in your codebase echo "=== Customer.io Pitfall Audit ===" echo "--- Checking for Date.now() without /1000 ---" grep -rn "created_at.*Date.now()" --include="*.ts" --include="*.js" \ | grep -v "/ 1000" || echo "OK" echo "--- Checking for new TrackClient inside functions ---" grep -rn "new TrackClient" --include="*.ts" --include="*.js" \ | grep -v "^.*const\|^.*let\|^.*export" || echo "OK" echo "--- Checking for dynamic event names ---" grep -rn "name:.*\`" --include="*.ts" --include="*.js" \ | grep "track\|Track" || echo "OK" echo "--- Checking for blocking await in routes ---" grep -rn "await.*cio\.\|await.*track\.\|await.*identify" --include="*.ts" \ | grep "router\.\|app\." || echo "Review these for fire-and-forget" ``` ## Resources - [Customer.io Best Practices](https://docs.customer.io/get-started/) - [Track API Reference](https://docs.customer.io/integrations/api/track/) - [customerio-node SDK](https://github.com/customerio/customerio-node) ## Next Steps After reviewing pitfalls, use `customerio-reliability-patterns` for fault tolerance.
Name Size