Skip to content

Measure the activation funnel and separate agent-driven forks - #942

Merged
kody-bot merged 2 commits into
mainfrom
cursor/activation-instrumentation-34ba
Jul 25, 2026
Merged

kody-bot merged 2 commits into
mainfrom
cursor/activation-instrumentation-34ba

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 25, 2026 •

Copy link
Copy Markdown
Member

We agreed activation is a package that ran successfully at least twice — the point where someone has something working unattended, rather than something they got running once during setup. This makes that measurable.

Baseline is perishable: every onboarding change from here on is unfalsifiable without a before. That's why this lands first.

What needed new storage, and what didn't

Most funnel steps were already derivable, so they're read where they live rather than duplicated:

Step Source
Signed up / verified users
Connected an agent mcp_agent_sessions
Forked a package community_forks

Two things were not derivable.

Run-derived milestones. package_runtime_runs is retention-pruned (packages/worker/src/app/retention.ts), so a milestone not captured when it happens cannot be recovered. user_activation_milestones records them once, on the way past. The migration backfills from the runs that still survive — that under-counts, and a partial baseline beats none.

Fork actor. A person clicking install and that person's agent calling community_fork were the same row. They are not the same activation signal, and we have a concrete reason to care: at least one 5-star community rating was left by a user's agent without the user knowing. Agent activity will inflate these numbers if it isn't separated. community_forks.actor is now set by both call paths — install.ts defaults to human, the MCP capability passes agent. Existing rows stay NULL because their origin is genuinely unknown.

Activation is counted per package, not per user: two different packages that each ran once is a user still in setup, not a user with something working.

Dashboard

Activation funnel and fork actor breakdown on /admin/insights

Seeded local data. The funnel is scaled against signups so drop-off is the thing you see, and the median is measured from email verification rather than signup so invite lag doesn't pollute it.

Manual testing caught one bug worth calling out: the subtitle read "Nobody has activated yet" while the funnel directly below showed two activated users. Backfilled milestones have no verification date to measure from, so a null median doesn't mean nobody activated. Fixed in the second commit, with a regression test.

Validation

npm run validate passes clean — format:check, lint, typecheck, 1290 unit tests, Playwright E2E, MCP E2E, primitives:check, migrations:check all exit 0. Migration 0093 applies cleanly against a from-scratch local D1 (all 99 migrations).

The repo's schema-coverage guardrails did their job here and required user_activation_milestones to be registered in accountUserDataTargets, so the new table is covered by account deletion and export like every other user-owned table.

Note: search-hidden-packages and meta-list-capabilities intermittently hit the 5s vitest timeout when validate runs alongside a dev server and browser. Both pass in isolation and in a quiet run; unrelated to this change, but worth knowing if CI is ever noisy on those two.

Follow-ups not in this PR

Time-to-activation is only measurable going forward for users who verify after this deploys. And agent_connected counts anyone with an MCP session row, which is a connection rather than a successful first call — worth tightening if that step turns out to be the drop-off the feedback suggests it is.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added activation milestone tracking, including successful package runs and package activation.
    • Added an Admin Insights activation funnel with activation timing metrics.
    • Added a “Who forks packages” breakdown distinguishing human and agent activity.
    • Package installations now record whether they were initiated by a person or an agent.
  • Bug Fixes

    • Improved activation metrics when timing data is unavailable.
    • Preserved activation history during account data management operations.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e11363c0-4fd7-4bbb-ba41-a243b11b0426

📥 Commits

Reviewing files that changed from the base of the PR and between 65de95a and 2a744ec.

📒 Files selected for processing (18)
  • packages/worker/client/routes/admin-insights.tsx
  • packages/worker/migrations/0094-activation-milestones.sql
  • packages/worker/src/app/account-data-targets.ts
  • packages/worker/src/app/admin-insights-data.node.test.ts
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/community/community-activity-service.node.test.ts
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/community/install.ts
  • packages/worker/src/community/repo.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/community/types.ts
  • packages/worker/src/mcp/capabilities/community/fork.ts
  • packages/worker/src/package-runtime/package-runtime-debug.ts
  • packages/worker/src/usage/activation.node.test.ts
  • packages/worker/src/usage/activation.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/mcp/capabilities/community/fork.ts
  • packages/worker/src/community/types.ts
  • packages/worker/src/app/account-data-targets.ts
  • packages/worker/src/package-runtime/package-runtime-debug.ts
  • packages/worker/src/community/install.ts
  • packages/worker/src/usage/activation.ts
  • packages/worker/src/community/repo.ts
  • packages/worker/src/app/admin-insights-data.ts

📝 Walkthrough

Walkthrough

The change adds persisted activation milestones, fork-actor attribution, activation aggregates to admin insights, and dashboard visualizations for activation funnels and fork sources. It also updates migration tracking, account data targets, runtime instrumentation, schemas, and tests.

Changes

Activation insights

Layer / File(s) Summary
Activation milestone lifecycle
packages/worker/migrations/0094-activation-milestones.sql, packages/worker/src/usage/*, packages/worker/src/package-runtime/*, packages/worker/src/app/account-data-targets.ts, tools/*
Adds and backfills activation milestone storage, records milestones after successful runs, includes the table in account data handling, and updates migration ledger validation fixtures.
Fork actor attribution
packages/worker/src/community/*, packages/worker/src/mcp/capabilities/community/fork.ts
Adds nullable human/agent fork attribution to community fork schemas, types, installation, persistence, service calls, and the MCP fork capability.
Activation insights pipeline and dashboard
packages/worker/src/app/loader-data.ts, packages/worker/src/app/admin-insights-data.ts, packages/worker/src/app/admin-insights-data.node.test.ts, packages/worker/client/routes/admin-insights.tsx
Adds activation loader data, parallel aggregation queries, median latency calculation, test coverage, an activation funnel, and fork-source chart cards.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PackageRuntime
  participant ActivationTracking
  participant ActivationDatabase
  participant AdminInsights
  participant InsightsDashboard
  PackageRuntime->>ActivationTracking: record successful package run
  ActivationTracking->>ActivationDatabase: store milestone and query run count
  ActivationTracking->>ActivationDatabase: store package_activated after second success
  AdminInsights->>ActivationDatabase: query milestones and fork actor counts
  AdminInsights->>InsightsDashboard: return activation aggregates
  InsightsDashboard->>InsightsDashboard: render funnel and fork-source charts
Loading

Possibly related PRs

  • kentcdodds/kody#599: Touches the community forking implementation extended here with actor persistence.
  • kentcdodds/kody#723: Touches the admin insights pipeline extended here with activation metrics.
  • kentcdodds/kody#763: Modifies the community forking flow and related service signature.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: activation funnel measurement and distinguishing agent-driven forks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/activation-instrumentation-34ba

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 25, 2026 20:26
cursoragent and others added 2 commits July 25, 2026 20:29
Activation is defined as a package that ran successfully at least twice —
the point where a user has something working unattended rather than
something they got running once during setup.

Most funnel steps were already derivable: signup and verification from
`users`, agent connection from `mcp_agent_sessions`, first fork from
`community_forks`. Two were not, and this adds exactly those.

Run-derived milestones need durable storage because `package_runtime_runs`
is retention-pruned, so a milestone not captured when it happens cannot be
recovered later. The migration backfills what still survives.

Fork actor matters because a person clicking install and that person's
agent calling `community_fork` were the same row. They are not the same
activation signal — we have at least one 5-star community rating left by a
user's agent without the user knowing, so agent activity can inflate these
numbers if it is not separated.

Adds an activation funnel and fork-actor breakdown to /admin/insights.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Backfilled milestones carry no verification date to measure from, so the
median can be null while users have genuinely activated. The subtitle read
"Nobody has activated yet" in that case, contradicting the funnel directly
below it.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/worker/src/app/admin-insights-data.ts (1)

213-213: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Migration needed to handle stale admin-insights:v2 cache entries for activation.

loadAdminInsightsData caches the full payload under the unchanged admin-insights:v2 key, while AdminInsightsLoaderData.activation is now required and populated by queryActivation(). Entries cached before this deploy will deserialize without activation unless the staleness handling/checkValue migration or cache-key bump handles format incompatibilities.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-insights-data.ts` at line 213, Update
loadAdminInsightsData and its admin-insights:v2 cache validation or versioning
so previously cached payloads missing the required
AdminInsightsLoaderData.activation field are treated as stale and regenerated
via queryActivation(db). Preserve valid current-format cache entries while
ensuring incompatible legacy entries are not returned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/client/routes/admin-insights.tsx`:
- Around line 140-188: Add a stable, unique React key to the outer div returned
by activation.steps.map in renderActivationFunnel, using the step identifier
already available on each entry, while leaving the funnel rendering unchanged.

In `@packages/worker/migrations/0093-activation-milestones.sql`:
- Around line 45-48: Update the package_run_succeeded backfill query in
migration 0093 to select the earliest successful package_runtime_runs row per
user and persist that row’s package_id instead of NULL. Preserve the existing
user grouping and first-success timestamp semantics while ensuring the selected
package_id comes from the same earliest run.

In `@packages/worker/src/community/community-flow-test-schema.ts`:
- Line 112: Add a best-effort additive ALTER for the actor column to the
community_forks schema migration, alongside the existing additiveAlters entries.
Match the column definition and constraints used by the CREATE TABLE schema,
while tolerating the already-exists case so existing and newly created databases
both remain compatible.

---

Nitpick comments:
In `@packages/worker/src/app/admin-insights-data.ts`:
- Line 213: Update loadAdminInsightsData and its admin-insights:v2 cache
validation or versioning so previously cached payloads missing the required
AdminInsightsLoaderData.activation field are treated as stale and regenerated
via queryActivation(db). Preserve valid current-format cache entries while
ensuring incompatible legacy entries are not returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e6fdfea1-f1c3-466f-a3b7-5638040cabb9

📥 Commits

Reviewing files that changed from the base of the PR and between 367cd15 and 65de95a.

📒 Files selected for processing (18)
  • packages/worker/client/routes/admin-insights.tsx
  • packages/worker/migrations/0093-activation-milestones.sql
  • packages/worker/src/app/account-data-targets.ts
  • packages/worker/src/app/admin-insights-data.node.test.ts
  • packages/worker/src/app/admin-insights-data.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/community/community-activity-service.node.test.ts
  • packages/worker/src/community/community-flow-test-schema.ts
  • packages/worker/src/community/install.ts
  • packages/worker/src/community/repo.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/community/types.ts
  • packages/worker/src/mcp/capabilities/community/fork.ts
  • packages/worker/src/package-runtime/package-runtime-debug.ts
  • packages/worker/src/usage/activation.node.test.ts
  • packages/worker/src/usage/activation.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json

Comment on lines +140 to +188
function renderActivationFunnel(activation: AdminInsightsActivation) {
const total = activation.steps[0]?.users ?? 0
return (
<div mix={css({ display: 'grid', gap: spacing.sm })}>
{activation.steps.map((entry) => {
const share = total > 0 ? entry.users / total : 0
return (
<div mix={css({ display: 'grid', gap: spacing.xs })}>
<div
mix={css({
display: 'flex',
justifyContent: 'space-between',
gap: spacing.sm,
fontSize: typography.fontSize.sm,
color: colors.text,
})}
>
<span>{activationStepLabels[entry.step]}</span>
<span mix={css({ color: colors.textMuted })}>
{formatIntegerNumber(entry.users)} · {formatPercentShare(share)}
</span>
</div>
<div
role="img"
aria-label={`${activationStepLabels[entry.step]}: ${entry.users} users, ${formatPercentShare(share)} of signups`}
mix={css({
height: '10px',
borderRadius: '999px',
background: colors.border,
overflow: 'hidden',
})}
>
<div
mix={css({
height: '100%',
width: `${Math.round(share * 100)}%`,
background:
entry.step === 'package_activated'
? chartColor.emerald
: chartColor.blue,
})}
/>
</div>
</div>
)
})}
</div>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing key on mapped funnel rows.

activation.steps.map(...) renders a <div> per step without a key, unlike the DonutChart slices elsewhere in this file which set key={slice.label}.

🔧 Proposed fix
 			{activation.steps.map((entry) => {
 				const share = total > 0 ? entry.users / total : 0
 				return (
-					<div mix={css({ display: 'grid', gap: spacing.xs })}>
+					<div key={entry.step} mix={css({ display: 'grid', gap: spacing.xs })}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function renderActivationFunnel(activation: AdminInsightsActivation) {
const total = activation.steps[0]?.users ?? 0
return (
<div mix={css({ display: 'grid', gap: spacing.sm })}>
{activation.steps.map((entry) => {
const share = total > 0 ? entry.users / total : 0
return (
<div mix={css({ display: 'grid', gap: spacing.xs })}>
<div
mix={css({
display: 'flex',
justifyContent: 'space-between',
gap: spacing.sm,
fontSize: typography.fontSize.sm,
color: colors.text,
})}
>
<span>{activationStepLabels[entry.step]}</span>
<span mix={css({ color: colors.textMuted })}>
{formatIntegerNumber(entry.users)} · {formatPercentShare(share)}
</span>
</div>
<div
role="img"
aria-label={`${activationStepLabels[entry.step]}: ${entry.users} users, ${formatPercentShare(share)} of signups`}
mix={css({
height: '10px',
borderRadius: '999px',
background: colors.border,
overflow: 'hidden',
})}
>
<div
mix={css({
height: '100%',
width: `${Math.round(share * 100)}%`,
background:
entry.step === 'package_activated'
? chartColor.emerald
: chartColor.blue,
})}
/>
</div>
</div>
)
})}
</div>
)
}
function renderActivationFunnel(activation: AdminInsightsActivation) {
const total = activation.steps[0]?.users ?? 0
return (
<div mix={css({ display: 'grid', gap: spacing.sm })}>
{activation.steps.map((entry) => {
const share = total > 0 ? entry.users / total : 0
return (
<div key={entry.step} mix={css({ display: 'grid', gap: spacing.xs })}>
<div
mix={css({
display: 'flex',
justifyContent: 'space-between',
gap: spacing.sm,
fontSize: typography.fontSize.sm,
color: colors.text,
})}
>
<span>{activationStepLabels[entry.step]}</span>
<span mix={css({ color: colors.textMuted })}>
{formatIntegerNumber(entry.users)} · {formatPercentShare(share)}
</span>
</div>
<div
role="img"
aria-label={`${activationStepLabels[entry.step]}: ${entry.users} users, ${formatPercentShare(share)} of signups`}
mix={css({
height: '10px',
borderRadius: '999px',
background: colors.border,
overflow: 'hidden',
})}
>
<div
mix={css({
height: '100%',
width: `${Math.round(share * 100)}%`,
background:
entry.step === 'package_activated'
? chartColor.emerald
: chartColor.blue,
})}
/>
</div>
</div>
)
})}
</div>
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-insights.tsx` around lines 140 - 188, Add
a stable, unique React key to the outer div returned by activation.steps.map in
renderActivationFunnel, using the step identifier already available on each
entry, while leaving the funnel rendering unchanged.

Comment on lines +45 to +48
SELECT user_id, 'package_run_succeeded', MIN(started_at), NULL
FROM package_runtime_runs
WHERE status = 'success'
GROUP BY user_id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Persist the backfilled first-success package ID.

This writes NULL for every historical package_run_succeeded record, permanently losing the package attribution that this table is meant to retain. Select the earliest successful run row per user, including its package_id.

Proposed fix
-SELECT user_id, 'package_run_succeeded', MIN(started_at), NULL
-FROM package_runtime_runs
-WHERE status = 'success'
-GROUP BY user_id;
+SELECT user_id, 'package_run_succeeded', started_at, package_id
+FROM (
+  SELECT
+    user_id,
+    package_id,
+    started_at,
+    ROW_NUMBER() OVER (
+      PARTITION BY user_id ORDER BY started_at, package_id
+    ) AS rn
+  FROM package_runtime_runs
+  WHERE status = 'success'
+)
+WHERE rn = 1;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/migrations/0093-activation-milestones.sql` around lines 45 -
48, Update the package_run_succeeded backfill query in migration 0093 to select
the earliest successful package_runtime_runs row per user and persist that row’s
package_id instead of NULL. Preserve the existing user grouping and
first-success timestamp semantics while ensuring the selected package_id comes
from the same earliest run.

listing_kody_id TEXT,
adopted_at TEXT,
adoption_note TEXT,
actor TEXT CHECK (actor IS NULL OR actor IN ('human', 'agent')),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Upgrade existing community_forks schemas with actor.

CREATE TABLE IF NOT EXISTS will not add this column to an existing database, and additiveAlters omits it. Since new inserts always include actor, pre-existing schemas will fail with “no column named actor.” Add a best-effort ALTER TABLE community_forks ADD COLUMN actor ... alongside the other additive alters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/community/community-flow-test-schema.ts` at line 112, Add
a best-effort additive ALTER for the actor column to the community_forks schema
migration, alongside the existing additiveAlters entries. Match the column
definition and constraints used by the CREATE TABLE schema, while tolerating the
already-exists case so existing and newly created databases both remain
compatible.

@cursor
cursor Bot force-pushed the cursor/activation-instrumentation-34ba branch from 65de95a to 2a744ec Compare July 25, 2026 20:57

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2a744ec. Configure here.

const row = await env.APP_DB.prepare(countSuccessfulRunsStatement)
.bind(input.userId, input.packageId)
.first<{ n: number }>()
if (Number(row?.n ?? 0) < 2) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Milestone before success count check

Medium Severity

recordSuccessfulPackageRun inserts the package_run_succeeded milestone before it counts successful rows in package_runtime_runs. finishPackageRuntimeRun still calls it when status is success even if the preceding status UPDATE failed, so a user can be counted as “Package ran once” with zero persisted successful runs, skewing the activation funnel.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2a744ec. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-942.kody-a99.workers.dev

Worker: kody-pr-942
D1: kody-pr-942-db
KV: kody-pr-942-oauth-kv

Mocks:

@kody-bot
kody-bot merged commit 8b335be into main Jul 25, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/activation-instrumentation-34ba branch July 25, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants