Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Customer.io local development workflow. Use when setting up local testing, dev/staging isolation, or mocking Customer.io for unit tests. Trigger: "customer.io local dev", "test customer.io locally", "customer.io dev environment", "customer.io sandbox", "mock customer.io".
.claude/skills/jeremylongshore-customerio-local-dev-loop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 17% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 53% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 43% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 77% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 42% | 0% |
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.
customerio-node installeddotenv or similar for environment variable loadingbash# .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_
typescript// 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); } }
typescript// __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);
typescript// 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:
bash# Load dev env and run integration tests npx dotenv -e .env.development -- npx vitest run tests/customerio.integration.test.ts
json// 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" } }
| Environment | Workspace Name | Event Prefix | Dry Run | |-------------|---------------|--------------|---------| | Unit tests | (mocked) | test_ | true | | Integration tests | myapp-dev | inttest_ | false | | Staging | myapp-staging | (none) | false | | Production | myapp-prod | (none) | false |
| Error | Cause | Solution | |-------|-------|----------| | Dev events in production | Wrong .env file loaded | Verify NODE_ENV and env file path | | Mock not intercepting | Import order issue | Mock customerio-node before importing your client module | | Test user pollution | No cleanup | Always suppress + destroy test users in afterAll |
After setting up local dev, proceed to customerio-sdk-patterns for production-ready patterns.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 15,982 | 10,252 | -36% | 1 | 1 | 0% | 3,670 | 4,620 | +26% | 0 | 0 | — |
case-02 | fail→fail | 12,317 | 10,923 | -11% | 1 | 1 | 0% | 2,982 | 4,592 | +54% | 0 | 0 | — |
case-03 | fail→pass | 17,446 | 10,181 | -42% | 1 | 1 | 0% | 3,772 | 4,396 | +17% | 0 | 0 | — |
case-04 | fail→pass | 10,328 | 4,788 | -54% | 1 | 1 | 0% | 2,032 | 3,099 | +53% | 0 | 0 | — |
case-05 | pass→pass | 9,841 | 5,857 | -40% | 1 | 1 | 0% | 1,904 | 3,222 | +69% | 0 | 0 | — |
case-06 | fail→pass | 10,545 | 5,097 | -52% | 1 | 1 | 0% | 2,263 | 3,243 | +43% | 0 | 0 | — |
case-07 | fail→fail | 10,244 | 6,569 | -36% | 1 | 1 | 0% | 2,004 | 3,365 | +68% | 0 | 0 | — |
case-08 | fail→fail | 14,428 | 13,036 | -10% | 1 | 1 | 0% | 2,495 | 4,469 | +79% | 0 | 0 | — |
case-09 | fail→pass | 8,757 | 2,935 | -66% | 1 | 1 | 0% | 1,504 | 2,664 | +77% | 0 | 0 | — |
case-10 | fail→fail | 10,028 | 4,799 | -52% | 1 | 1 | 0% | 1,715 | 3,087 | +80% | 0 | 0 | — |
case-11 | fail→pass | 9,260 | 1,601 | -83% | 1 | 1 | 0% | 1,689 | 2,397 | +42% | 0 | 0 | — |
case-12 | pass→pass | 5,458 | 2,634 | -52% | 1 | 1 | 0% | 928 | 2,553 | +175% | 0 | 0 | — |
case-13 | pass→pass | 6,816 | 4,249 | -38% | 1 | 1 | 0% | 1,218 | 2,808 | +131% | 0 | 0 | — |
case-14 | pass→pass | 12,032 | 9,022 | -25% | 1 | 1 | 0% | 2,231 | 3,916 | +76% | 0 | 0 | — |
case-15 | fail→pass | 11,620 | 8,895 | -23% | 1 | 1 | 0% | 1,928 | 3,797 | +97% | 0 | 0 | — |
case-16 | pass→pass | 5,742 | 2,144 | -63% | 1 | 1 | 0% | 1,227 | 2,548 | +108% | 0 | 0 | — |
case-17 | fail→pass | 8,270 | 3,618 | -56% | 1 | 1 | 0% | 1,664 | 2,770 | +66% | 0 | 0 | — |
case-18 | fail→fail | 7,726 | 5,908 | -24% | 1 | 1 | 0% | 1,549 | 3,228 | +108% | 0 | 0 | — |
case-19 | pass→pass | 14,743 | 11,124 | -25% | 1 | 1 | 0% | 3,030 | 4,348 | +43% | 0 | 0 | — |
case-20 | pass→pass | 16,349 | 13,997 | -14% | 1 | 1 | 0% | 2,833 | 4,788 | +69% | 0 | 0 | — |
case-21 | fail→fail | 12,977 | 8,647 | -33% | 1 | 1 | 0% | 2,368 | 3,605 | +52% | 0 | 0 | — |
case-22 | pass→pass | 12,935 | 6,690 | -48% | 1 | 1 | 0% | 2,370 | 3,444 | +45% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.