Claude-code-plugins-plus-skills customerio-local-dev-loop
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/customerio-pack/skills/customerio-local-dev-loop" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-customerio-local-dev-loop && rm -rf "$T"
manifest:
plugins/saas-packs/customerio-pack/skills/customerio-local-dev-loop/SKILL.mdsource content
Customer.io Local Dev Loop
Overview
Set up an efficient local development workflow for Customer.io: environment isolation via separate workspaces, a dry-run client for safe development, test mocks for unit tests, and prefixed events that never pollute production data.
Prerequisites
installedcustomerio-node- Separate Customer.io workspace for development (recommended — free workspaces available)
or similar for environment variable loadingdotenv
Instructions
Step 1: Environment Configuration
# .env.development CUSTOMERIO_SITE_ID=dev-site-id CUSTOMERIO_TRACK_API_KEY=dev-track-key CUSTOMERIO_APP_API_KEY=dev-app-key CUSTOMERIO_REGION=us CUSTOMERIO_DRY_RUN=false CUSTOMERIO_EVENT_PREFIX=dev_ # .env.test CUSTOMERIO_SITE_ID=not-needed CUSTOMERIO_TRACK_API_KEY=not-needed CUSTOMERIO_APP_API_KEY=not-needed CUSTOMERIO_DRY_RUN=true CUSTOMERIO_EVENT_PREFIX=test_
Step 2: Environment-Aware Client
// lib/customerio-dev.ts import { TrackClient, APIClient, RegionUS, RegionEU } from "customerio-node"; interface CioConfig { siteId: string; trackApiKey: string; appApiKey: string; region: typeof RegionUS | typeof RegionEU; dryRun: boolean; eventPrefix: string; } function loadConfig(): CioConfig { return { siteId: process.env.CUSTOMERIO_SITE_ID ?? "", trackApiKey: process.env.CUSTOMERIO_TRACK_API_KEY ?? "", appApiKey: process.env.CUSTOMERIO_APP_API_KEY ?? "", region: process.env.CUSTOMERIO_REGION === "eu" ? RegionEU : RegionUS, dryRun: process.env.CUSTOMERIO_DRY_RUN === "true", eventPrefix: process.env.CUSTOMERIO_EVENT_PREFIX ?? "", }; } export class DevTrackClient { private client: TrackClient | null = null; private config: CioConfig; private log: typeof console.log; constructor() { this.config = loadConfig(); this.log = console.log.bind(console); if (!this.config.dryRun) { this.client = new TrackClient( this.config.siteId, this.config.trackApiKey, { region: this.config.region } ); } } async identify(userId: string, attributes: Record<string, any>) { const prefixedId = `${this.config.eventPrefix}${userId}`; if (this.config.dryRun) { this.log("[DRY RUN] identify:", prefixedId, attributes); return; } return this.client!.identify(prefixedId, attributes); } async track(userId: string, eventName: string, data?: Record<string, any>) { const prefixedId = `${this.config.eventPrefix}${userId}`; const prefixedEvent = `${this.config.eventPrefix}${eventName}`; if (this.config.dryRun) { this.log("[DRY RUN] track:", prefixedId, prefixedEvent, data); return; } return this.client!.track(prefixedId, { name: prefixedEvent, data, }); } async suppress(userId: string) { const prefixedId = `${this.config.eventPrefix}${userId}`; if (this.config.dryRun) { this.log("[DRY RUN] suppress:", prefixedId); return; } return this.client!.suppress(prefixedId); } }
Step 3: Test Mocks for Unit Tests
// __mocks__/customerio-node.ts (for vitest/jest auto-mocking) import { vi } from "vitest"; export const TrackClient = vi.fn().mockImplementation(() => ({ identify: vi.fn().mockResolvedValue(undefined), track: vi.fn().mockResolvedValue(undefined), trackAnonymous: vi.fn().mockResolvedValue(undefined), suppress: vi.fn().mockResolvedValue(undefined), destroy: vi.fn().mockResolvedValue(undefined), mergeCustomers: vi.fn().mockResolvedValue(undefined), })); export const APIClient = vi.fn().mockImplementation(() => ({ sendEmail: vi.fn().mockResolvedValue({ delivery_id: "mock-delivery-123" }), sendPush: vi.fn().mockResolvedValue({ delivery_id: "mock-push-456" }), triggerBroadcast: vi.fn().mockResolvedValue(undefined), })); export const RegionUS = "us"; export const RegionEU = "eu"; export const SendEmailRequest = vi.fn().mockImplementation((data) => data); export const SendPushRequest = vi.fn().mockImplementation((data) => data);
Step 4: Integration Test with Real API
// tests/customerio.integration.test.ts import { describe, it, expect, afterAll } from "vitest"; import { TrackClient, RegionUS } from "customerio-node"; const TEST_PREFIX = `test_${Date.now()}_`; const testUserIds: string[] = []; const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); function testUserId(label: string): string { const id = `${TEST_PREFIX}${label}`; testUserIds.push(id); return id; } describe("Customer.io Integration", () => { afterAll(async () => { // Clean up all test users for (const id of testUserIds) { await cio.suppress(id).catch(() => {}); await cio.destroy(id).catch(() => {}); } }); it("should identify a user", async () => { const id = testUserId("identify"); await expect( cio.identify(id, { email: `${id}@test.example.com` }) ).resolves.not.toThrow(); }); it("should track an event", async () => { const id = testUserId("track"); await cio.identify(id, { email: `${id}@test.example.com` }); await expect( cio.track(id, { name: "test_event", data: { step: 1 } }) ).resolves.not.toThrow(); }); it("should reject invalid credentials", async () => { const badClient = new TrackClient("bad-id", "bad-key", { region: RegionUS, }); await expect( badClient.identify("x", { email: "x@test.com" }) ).rejects.toThrow(); }); });
Run integration tests only against your dev workspace:
# Load dev env and run integration tests npx dotenv -e .env.development -- npx vitest run tests/customerio.integration.test.ts
Step 5: Dev Scripts
// package.json scripts { "scripts": { "cio:verify": "dotenv -e .env.development -- tsx scripts/verify-customerio.ts", "cio:test": "dotenv -e .env.development -- vitest run tests/customerio.integration.test.ts", "cio:test:dry": "CUSTOMERIO_DRY_RUN=true vitest run tests/customerio" } }
Workspace Isolation Strategy
| Environment | Workspace Name | Event Prefix | Dry Run |
|---|---|---|---|
| Unit tests | (mocked) | | true |
| Integration tests | | | false |
| Staging | | (none) | false |
| Production | | (none) | false |
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Dev events in production | Wrong file loaded | Verify and env file path |
| Mock not intercepting | Import order issue | Mock before importing your client module |
| Test user pollution | No cleanup | Always suppress + destroy test users in |
Resources
Next Steps
After setting up local dev, proceed to
customerio-sdk-patterns for production-ready patterns.