From sdk-instrumenter
Identify and instrument business processes in a Next.js app with the Measure SDK (@measuremetrics/measure-sdk). Use when asked to 'instrument this app', 'add Measure', 'install measure-sdk', 'add metrics tracking', 'instrument business processes', or when onboarding a Next.js B2B SaaS app to Measure. Reads the codebase first to understand the business model, then leads a Kimball-style metric planning conversation before writing any code. Uses agent teams for parallel codebase analysis and adversarial plan review.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sdk-instrumenter:instrumentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Instrument a Next.js B2B SaaS app with `@measuremetrics/measure-sdk`. This is a **planning-first** process — understand the business before touching code. Uses agent teams to parallelize discovery and stress-test the instrumentation plan.
Instrument a Next.js B2B SaaS app with @measuremetrics/measure-sdk. This is a planning-first process — understand the business before touching code. Uses agent teams to parallelize discovery and stress-test the instrumentation plan.
References (read as needed, not upfront):
references/metric-planning.md — Kimball process, metric catalog, schema patterns, push-back checklistreferences/sdk-patterns.md — SDK init, after() patterns, code examples (Phase 4 only)Read the codebase to determine if this is a B2B SaaS business. Check:
next.config.* — this skill targets Next.js apps only.plan, price, status, trial)stripeCustomerId, priceId, checkout flows)active, churned, trialing, cancelled)If the app is not B2B SaaS (no subscription model, no multi-tenant structure, consumer app, e-commerce, marketplace), stop and tell the developer: "Measure's instrumentation skill currently targets B2B SaaS apps. Your app looks like [what it looks like]. You can still use the Measure API directly — here's the SDK."
If it is B2B SaaS, proceed. Summarize what you found for the developer.
Spawn four Explore agents in parallel to scan different domains of the codebase simultaneously. Each agent produces a structured report. This is a Parallel Investigation pattern — the sub-tasks are independent.
Agent 1 — Schema & Data Model:
Research only — do NOT write code.
Read the database schema (Prisma, Drizzle, or raw SQL migrations).
Report:
- All tables/models related to customers, companies, organizations, tenants
- Subscription/plan/billing models — fields, types, enums
- User model and its relationship to company/org
- Status enums and lifecycle fields (created_at, churned_at, trial_ends_at, etc.)
- Price storage: where, what unit (cents? dollars?), how plans map to prices
- Any integration ID columns (stripeCustomerId, hubspotCompanyId, clerkOrgId)
Output as structured findings, not prose.
Agent 2 — Server Actions & API Routes:
Research only — do NOT write code.
Find all server actions (files in app/**/actions.ts or similar) and API routes (app/api/**).
For each mutation function, report:
- Function name and file path
- What it creates/updates/deletes
- What arguments it takes (especially price, plan, status)
- Whether it has access to "old" values before mutation (critical for deltas)
- Whether it uses transactions or has error handling
- Any existing tracking/analytics calls
Output as a table: function | file | mutation type | entities affected | price accessible?
Agent 3 — Webhooks & Integration Points:
Research only — do NOT write code.
Find all webhook handlers (app/api/webhooks/**) and integration configuration.
Report:
- Clerk webhooks: what events are handled, what they create in the DB
- Stripe webhooks: what events are handled, subscription lifecycle coverage
- Any other integrations (HubSpot, QuickBooks, etc.)
- OAuth flows or integration setup code
- .env.example or config for integration API keys
Determine: which business domains are owned by integrations vs the app?
Agent 4 — Middleware & Engagement:
Research only — do NOT write code.
Read middleware.ts and any auth/session handling code.
Report:
- What middleware.ts does (auth checks, redirects, etc.)
- Runtime: Edge or Node.js? (affects SDK compatibility)
- How user identity is resolved (Clerk? cookies? session?)
- Any existing page view or engagement tracking
- Login event tables or session tables (if they exist)
- Feature usage tracking patterns (feature flags, event logging)
Output: engagement instrumentation feasibility assessment.
Collect all four agent reports. Synthesize into a business process map — this is your job as the lead, not a raw concatenation.
Look for:
subscription.created, that domain belongs to Stripe, not to app instrumentation.Present the synthesized map:
Business processes this app owns:
├── Customer signup
│ ├── Code: src/app/actions/customers.ts:createCustomer()
│ ├── Creates: Company + Subscription (in transaction)
│ └── Price: subscription.price (cents)
├── Plan changes
│ ├── Code: src/app/actions/customers.ts:changePlan()
│ ├── Updates: Subscription.plan + Subscription.price
│ └── ⚠️ Old price: queried before update (confirmed by Agent 2)
├── Cancellation
│ ├── Code: src/app/actions/customers.ts:cancelSubscription()
│ └── Sets: Subscription.status = 'cancelled', Company.status = 'churned'
├── Feature usage
│ ├── Code: src/app/actions/product.ts:trackFeatureUsage()
│ └── Records: FeatureEvent with feature slug + user + company
└── Page views
├── Code: middleware.ts (Edge runtime)
└── Auth: Clerk, userId from session
Owned by external systems (don't instrument):
├── Billing → Stripe (webhook: subscription.created/updated/deleted)
├── User identity → Clerk (webhook: user.created)
└── Expenses → not connected
Read references/metric-planning.md for the B2B SaaS metric catalog and dependency trees.
Based on the business process map, recommend specific metrics — each traced to the code path you found. Be specific about what the developer gets and what's missing:
Based on what I found in your codebase, here's what I'd recommend:
From your subscription lifecycle:
→ new_mrr, expansion_mrr, contraction_mrr, churned_mrr
→ new_customers, churned_customers
→ Measure computes from these: ARR, NRR, Quick Ratio, Gross Churn, ARPA
From middleware:
→ page_views → Measure computes DAU/MAU
From your core product interaction:
→ [whatever the core value-creation event is — e.g., "report generated",
"deal logged", "metrics computed"]
→ This is your activation signal — did the user get value?
I noticed Stripe webhooks handle subscription.created and subscription.updated.
If Stripe is your billing system of record, it should own MRR components through
a Measure integration connector — not app instrumentation. Which is authoritative
for billing: your app's server actions or Stripe?
Always identify the core value-creation event. Page views tell you someone showed up. The core product interaction tells you they got value. If the developer says "just page views," push back: "What action tells you a user actually got value from your product? That's the metric that enables activation rate."
Recommend full funnels. When you identify an onboarding flow (signup → setup → activation → conversion), recommend metrics at each stage. Partial funnels can't compute conversion rates. Always recommend paired metrics: trial_started + trial_converted, onboarding_started + onboarding_completed.
Ask the developer:
Read the push-back checklist in references/metric-planning.md. Do NOT proceed if:
changePlan() overwrites the plan without preserving the old price, you can't compute expansion/contraction deltas.churned_at doesn't exist, you know they churned but not when.getMemberId helper handles this by catching NotFoundError and creating the member). If none exist, flag it.Ask the developer to resolve ambiguities before proceeding.
For each agreed-upon business question, trace the dependency tree:
"What's our ARR?"
→ ARR = total_mrr × 12 (calculated — Measure computes)
→ total_mrr = Σ(new + expansion - contraction - churn)
├── new_mrr ← createCustomer() (instrument)
├── expansion_mrr ← changePlan() (instrument)
├── contraction_mrr ← changePlan() (instrument)
└── churned_mrr ← cancelSubscription() (instrument)
Present the full model separating three categories:
| Metric | Type | Source |
|---|---|---|
new_mrr | Base — instrument from app | createCustomer() |
expansion_mrr | Base — instrument from app | changePlan() |
| ARR | Calculated — Measure computes | total_mrr × 12 |
| NRR | Calculated — Measure computes | retention formula |
| Gross Margin | Calculated — needs integration | Stripe + QuickBooks |
For each base metric, document the full instrumentation specification:
Base metric: new_mrr
→ Trigger: new subscription created
→ Code path: src/app/actions/customers.ts:createCustomer()
→ Amount: subscription.price (cents → divide by 100)
→ Entity link: company.id → Customer durable_key
→ Old price accessible? N/A (new subscription)
→ Timestamp: now()
→ Status: Ready
If a mutation point cannot be found, report it and ask:
updatePlan()?"Before presenting the final plan, spawn a skeptic agent to challenge it. This is an Adversarial Debate pattern — catches blind spots before the developer approves.
Skeptic agent prompt:
Research only — do NOT write code.
Review the following instrumentation plan for a B2B SaaS app using Measure SDK.
[paste the metric model table and base metric → code path mappings]
Challenge this plan using these techniques:
- For each base metric: is the amount correct? Is the old value accessible for
deltas? Could this double-count with an integration?
- For entity links: is the durable_key stable? Could it change?
- For calculated metrics: do the formulas have all required inputs? Are there
edge cases (division by zero for NRR in period 1, Quick Ratio with no churn)?
- Grain check: is each adjustment exactly one business event, or could one
function call produce multiple adjustments that should be atomic?
- Multi-path counting: for count metrics, can the same entity trigger the metric
from more than one instrumented code path? Trace the full entity lifecycle —
creation, upgrade, downgrade, churn — and verify each stage is counted exactly
once across all paths. Freemium-to-paid and trial-to-active are common traps.
- Data quality: what happens if the app crashes between the DB write and the
Measure API call? What data is lost?
- Integration conflicts: any risk of double-counting if an integration is
connected later?
Output: structured list of concerns, ranked by severity.
Review the skeptic's findings. Address valid concerns by adjusting the plan or flagging them for the developer. Dismiss concerns that aren't applicable.
Present the final plan with a summary table and all flagged gaps:
| Base Metric | Code Path | Amount | Entity Link | Status |
|---|---|---|---|---|
new_mrr | createCustomer() | centsToDollars(price) | company.id | Ready |
expansion_mrr | changePlan() | centsToDollars(new - old) | company.id | Ready — old price queried before update |
churned_mrr | cancelSubscription() | centsToDollars(price) | company.id | Ready |
page_views | middleware.ts | 1 | user.id | ⚠️ Edge runtime |
Include any concerns from the adversarial review that the developer should know about.
Re-surface any unresolved blocking questions from Phase 1 before asking for approval. These are blocking — do not proceed without answers:
Get explicit approval before writing any code.
Before instrumenting code, the developer needs a Measure account with entities and metrics configured. This phase handles account setup and resource provisioning.
Ask the developer:
"Do you have a Measure account, or do we need to set one up?"
Existing account:
.env.local)measure.metrics.listPaginated() — if it succeeds, you're connectedNew account:
production, development)Either way, you need these four values before proceeding:
MEASURE_API_KEY=...
MEASURE_API_URL=...
MEASURE_ORG_ID=...
MEASURE_ENV_ID=...
From the metric model (Phase 2), create entity definitions with the properties you identified as useful for slicing metrics. Run these as a one-time setup script (e.g., scripts/measure-setup.ts) or in a REPL — these are admin operations, not application runtime code.
For a typical B2B SaaS app, this means:
// Customer entity — the billing/subscription unit
await measure.entities.create({
name: 'Customer',
cardinality: 'unbounded',
properties: [
{ name: 'plan', property_type: 'categorical', is_filterable: true, is_facetable: true },
{ name: 'status', property_type: 'categorical', is_filterable: true, is_facetable: true },
],
});
// User entity — the identity unit (if tracking engagement)
await measure.entities.create({
name: 'User',
cardinality: 'unbounded',
properties: [],
});
Adapt the entity names and properties to match what you found in the codebase. Entity names here must match the keys used in entity_member_links in the instrumentation code.
Create each base metric from the approved metric model. Set can_decrement based on whether the metric can go negative (MRR components can, counts typically can't). Link allowed entities.
// Example: create base metrics
const customerEntity = await measure.entities.listPaginated();
const custEntityId = customerEntity.data.find(e => e.name === 'Customer')!.id;
await measure.metrics.create({
name: 'new_mrr',
description: 'MRR from new subscriptions',
unit: 'currency',
precision_recorded: 2,
can_decrement: false,
allowed_entities: [{ entity_id: custEntityId, is_required: false }],
});
// contraction_mrr: can_decrement is FALSE because the amount is always
// the positive magnitude of the contraction (e.g., $50 lost), not a
// negative number. The metric name conveys the direction.
await measure.metrics.create({
name: 'contraction_mrr',
description: 'MRR lost from plan downgrades',
unit: 'currency',
precision_recorded: 2,
can_decrement: false,
allowed_entities: [{ entity_id: custEntityId, is_required: false }],
});
// Repeat for expansion_mrr, churned_mrr, new_customers,
// churned_customers, trial_started, trial_converted, page_views, etc.
If the SDK supports calculatedMetrics.create(), define the derived metrics from the dependency trees in Phase 2:
If calculated metrics aren't available yet, document what the developer should create manually in the Measure dashboard.
Confirm all resources exist:
Only after Phase 3 resources are provisioned. Read references/sdk-patterns.md for code patterns.
npm install @measuremetrics/measure-sdk, bun add, pnpm add, etc. — detect from the lockfile). For browser-side tracking, there's also @measuremetrics/measure-browser.node_modules/@measuremetrics/measure-sdk/examples/) and type definitions to confirm the reference patterns match. The SDK exports typed error classes (NotFoundError, RateLimitError, etc.) — use instanceof checks, not status code inspection.serverExternalPackages: ['@measuremetrics/measure-sdk'] to next.config.ts.lib/measure.ts with client init + ID resolution helpers (see references/sdk-patterns.md).env.example: MEASURE_API_KEY, MEASURE_API_URL, MEASURE_ORG_ID, MEASURE_ENV_IDMEASURE_API_KEY is not set, disable tracking silently instead of crashing the app. See the isConfigured pattern in references/sdk-patterns.md.export const runtime = 'nodejs'. If it defaults to Edge, either set Node.js explicitly or confirm the SDK is Edge-compatible. Don't leave this as "needs verification." See the runtime tradeoff notes in references/metric-planning.md.For each approved business process, use a subagent to write the instrumentation. The lead (you) orchestrates and reviews.
When to use subagents vs do it yourself:
Subagent prompt template:
Read references/sdk-patterns.md in the skill directory first.
Instrument the following business process with Measure SDK:
- Business process: [name]
- Code path: [file:function]
- Metrics to publish: [list with amounts and entity links]
- SDK patterns: use after() for non-blocking, catch and log errors inside after()
blocks (never let tracking errors propagate to the user's request),
entity member creation if needed
Write the instrumentation code. Do not change the function's business logic —
only add Measure tracking calls inside after() blocks.
After writing, document results in docs/measure-instrumentation.md with:
process name, code path, metrics, grain, amount, entity link, status, notes.
After each subagent completes (or after you instrument directly), verify:
after() — never blocks the business operationafter() catches and logs with context (metric name, entity ID) — never throws to the user's requestentity_member_links keys use entity names (e.g., "Customer") for readability — the API also accepts entity IDs but names are preferredcentsToDollars() for cents, pass decimals directly — never raw / 100)docs/measure-instrumentation.md is updatedAfter business processes are instrumented, address existing data:
references/sdk-patterns.md for the taxonomy.docs/measure-instrumentation.md.Present the seed script for review. It's a one-time operation that affects production data.
Pick the next process from the approved plan. Continue until all are instrumented or blocked.
After all processes are done:
tsc --noEmit (or the project's type-check command) to confirm no type errors were introduced.docs/measure-instrumentation.md — is coverage complete? Any processes marked "Blocked" that need the developer's input?entity_member_links accepts both UUIDs and durable keys as member values — the API resolves durable keys automatically. Entity keys accept both names and UUIDs.npx claudepluginhub measuremetrics/agent-skills --plugin sdk-instrumenterCreates a LaunchDarkly metric for experiments or rollouts. Instruments events (SDK setup, .env) first, then creates and verifies the metric.
Generates SDK-specific instrumentation guides from tracking plans for 24 analytics tools like Segment, Amplitude, PostHog, Sentry. Provides identify, group, track code templates in .telemetry/instrument.md.
Produces complete metrics specs for product areas: names, formulas, data sources, segmentation, SQL/event tracking, thresholds. Use for KPI definition or feature instrumentation.