feat: locked-feature upgrade prompts route to billing + track upgrade interest (v0.81.0)

UpgradeDialog's CTA now sends users to Settings > Billing (with a feature-specific banner) instead of a generic support-ticket prefill, and fires a best-effort tracking beacon recording which feature triggered the click. New feature_upgrade_clicks table + Admin > Insights chart ("Most-requested locked features") gives admins visibility into which locked features actually drive upgrade interest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-24 14:12:12 +02:00
parent 1fe379bcdc
commit ce7741c0b6
14 changed files with 6392 additions and 17 deletions
@@ -0,0 +1,8 @@
CREATE TABLE "feature_upgrade_clicks" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"feature_key" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "feature_upgrade_clicks" ADD CONSTRAINT "feature_upgrade_clicks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
File diff suppressed because it is too large Load Diff
@@ -456,6 +456,13 @@
"when": 1784885885146,
"tag": "0064_thick_drax",
"breakpoints": true
},
{
"idx": 65,
"version": "7",
"when": 1784894764837,
"tag": "0065_cooing_carnage",
"breakpoints": true
}
]
}
+14
View File
@@ -18,3 +18,17 @@ export const featureFlags = pgTable("feature_flags", {
export const featureFlagsRelations = relations(featureFlags, ({ one }) => ({
updatedBy: one(users, { fields: [featureFlags.updatedById], references: [users.id] }),
}));
// One row per "I'm interested" click from a locked-feature upgrade dialog —
// lets admins see which locked features are actually driving upgrade
// interest (Admin -> Insights), not just that upgrades happen.
export const featureUpgradeClicks = pgTable("feature_upgrade_clicks", {
id: text("id").primaryKey(),
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
featureKey: text("feature_key").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
export const featureUpgradeClicksRelations = relations(featureUpgradeClicks, ({ one }) => ({
user: one(users, { fields: [featureUpgradeClicks.userId], references: [users.id] }),
}));