Skip to content

Commit a13f395

Browse files
committed
fixed event tracking on subs stuff
1 parent fbd0ed5 commit a13f395

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

apps/web/src/convex/analyticsEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ export const AnalyticsEvents = {
44

55
CHECKOUT_STARTED: 'checkout_started',
66
SUBSCRIPTION_CREATED: 'subscription_created',
7+
SUBSCRIPTION_UPDATED: 'subscription_updated',
8+
SUBSCRIPTION_CANCELED: 'subscription_canceled',
79
BILLING_PORTAL_OPENED: 'billing_portal_opened',
810
USAGE_LIMIT_REACHED: 'usage_limit_reached',
911

apps/web/src/convex/instances/mutations.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,32 @@ export const updateStorageUsed = mutation({
194194
}
195195
});
196196

197+
export const setSubscriptionState = mutation({
198+
args: {
199+
instanceId: v.id('instances'),
200+
plan: v.union(v.literal('pro'), v.literal('free'), v.literal('none')),
201+
status: v.union(
202+
v.literal('active'),
203+
v.literal('trialing'),
204+
v.literal('canceled'),
205+
v.literal('none')
206+
),
207+
productId: v.optional(v.string()),
208+
currentPeriodEnd: v.optional(v.number()),
209+
canceledAt: v.optional(v.number())
210+
},
211+
handler: async (ctx, args) => {
212+
await ctx.db.patch(args.instanceId, {
213+
subscriptionPlan: args.plan,
214+
subscriptionStatus: args.status,
215+
subscriptionProductId: args.productId,
216+
subscriptionCurrentPeriodEnd: args.currentPeriodEnd,
217+
subscriptionCanceledAt: args.canceledAt,
218+
subscriptionUpdatedAt: Date.now()
219+
});
220+
}
221+
});
222+
197223
export const upsertCachedResources = mutation({
198224
args: {
199225
instanceId: v.id('instances'),

apps/web/src/convex/schema.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ export default defineSchema({
5656
latestBtcaVersion: v.optional(v.string()),
5757
latestOpencodeVersion: v.optional(v.string()),
5858
lastVersionCheck: v.optional(v.number()),
59+
subscriptionPlan: v.optional(v.union(v.literal('pro'), v.literal('free'), v.literal('none'))),
60+
subscriptionStatus: v.optional(
61+
v.union(
62+
v.literal('active'),
63+
v.literal('trialing'),
64+
v.literal('canceled'),
65+
v.literal('none')
66+
)
67+
),
68+
subscriptionProductId: v.optional(v.string()),
69+
subscriptionCurrentPeriodEnd: v.optional(v.number()),
70+
subscriptionCanceledAt: v.optional(v.number()),
71+
subscriptionUpdatedAt: v.optional(v.number()),
5972
storageUsedBytes: v.optional(v.number()),
6073
lastActiveAt: v.optional(v.number()),
6174
provisionedAt: v.optional(v.number()),

apps/web/src/convex/usage.ts

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Autumn } from 'autumn-js';
22
import { v } from 'convex/values';
33

4+
import type { Doc } from './_generated/dataModel.js';
45
import { internal } from './_generated/api.js';
5-
import { action } from './_generated/server.js';
6+
import { action, type ActionCtx } from './_generated/server.js';
67
import { AnalyticsEvents } from './analyticsEvents.js';
8+
import { instances } from './apiHelpers.js';
79
import { requireInstanceOwnershipAction } from './authHelpers.js';
810

911
type FeatureMetrics = {
@@ -60,6 +62,16 @@ type BillingSummaryResult = {
6062

6163
type SessionResult = { url: string };
6264

65+
type SubscriptionPlan = 'pro' | 'free' | 'none';
66+
type SubscriptionStatus = 'active' | 'trialing' | 'canceled' | 'none';
67+
type SubscriptionSnapshot = {
68+
plan: SubscriptionPlan;
69+
status: SubscriptionStatus;
70+
productId?: string;
71+
currentPeriodEnd?: number | null;
72+
canceledAt?: number | null;
73+
};
74+
6375
const SANDBOX_IDLE_MINUTES = 2;
6476
const CHARS_PER_TOKEN = 4;
6577
const FEATURE_IDS = {
@@ -209,6 +221,83 @@ function getActiveProduct(
209221
return null;
210222
}
211223

224+
function getSubscriptionSnapshot(
225+
activeProduct:
226+
| {
227+
id: string;
228+
status?: string;
229+
current_period_end?: number | null;
230+
canceled_at?: number | null;
231+
}
232+
| null
233+
): SubscriptionSnapshot {
234+
if (!activeProduct) {
235+
return { plan: 'none', status: 'none' };
236+
}
237+
238+
const plan: SubscriptionPlan =
239+
activeProduct.id === 'btca_pro' ? 'pro' : activeProduct.id === 'free_plan' ? 'free' : 'none';
240+
const status: SubscriptionStatus = activeProduct.status
241+
? (activeProduct.status as SubscriptionStatus)
242+
: 'none';
243+
244+
return {
245+
plan,
246+
status,
247+
productId: activeProduct.id,
248+
currentPeriodEnd: activeProduct.current_period_end ?? undefined,
249+
canceledAt: activeProduct.canceled_at ?? undefined
250+
};
251+
}
252+
253+
async function syncSubscriptionState(
254+
ctx: ActionCtx,
255+
instance: Doc<'instances'>,
256+
snapshot: SubscriptionSnapshot
257+
): Promise<void> {
258+
const previousPlan: SubscriptionPlan = instance.subscriptionPlan ?? 'none';
259+
const previousStatus: SubscriptionStatus = instance.subscriptionStatus ?? 'none';
260+
261+
if (previousPlan === snapshot.plan && previousStatus === snapshot.status) {
262+
return;
263+
}
264+
265+
await ctx.runMutation(instances.mutations.setSubscriptionState, {
266+
instanceId: instance._id,
267+
plan: snapshot.plan,
268+
status: snapshot.status,
269+
productId: snapshot.productId,
270+
currentPeriodEnd: snapshot.currentPeriodEnd ?? undefined,
271+
canceledAt: snapshot.canceledAt ?? undefined
272+
});
273+
274+
const properties = {
275+
instanceId: instance._id,
276+
plan: snapshot.plan,
277+
status: snapshot.status,
278+
previousPlan,
279+
previousStatus,
280+
productId: snapshot.productId ?? null,
281+
currentPeriodEnd: snapshot.currentPeriodEnd ?? null,
282+
canceledAt: snapshot.canceledAt ?? null
283+
};
284+
285+
const event =
286+
previousPlan !== 'pro' &&
287+
snapshot.plan === 'pro' &&
288+
(snapshot.status === 'active' || snapshot.status === 'trialing')
289+
? AnalyticsEvents.SUBSCRIPTION_CREATED
290+
: previousPlan === 'pro' && snapshot.plan !== 'pro'
291+
? AnalyticsEvents.SUBSCRIPTION_CANCELED
292+
: AnalyticsEvents.SUBSCRIPTION_UPDATED;
293+
294+
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
295+
distinctId: instance.clerkId,
296+
event,
297+
properties
298+
});
299+
}
300+
212301
async function checkFeature(args: {
213302
customerId: string;
214303
featureId: string;
@@ -275,6 +364,7 @@ export const ensureUsageAvailable = action({
275364
: undefined)
276365
});
277366
const activeProduct = getActiveProduct(autumnCustomer.products);
367+
await syncSubscriptionState(ctx, instance, getSubscriptionSnapshot(activeProduct));
278368
if (!activeProduct) {
279369
return {
280370
ok: false,
@@ -508,6 +598,14 @@ export const getBillingSummary = action({
508598
? (activeProduct.status as 'active' | 'trialing' | 'canceled')
509599
: 'none';
510600

601+
await syncSubscriptionState(ctx, instance, {
602+
plan,
603+
status,
604+
productId: activeProduct?.id ?? undefined,
605+
currentPeriodEnd: activeProduct?.current_period_end ?? undefined,
606+
canceledAt: activeProduct?.canceled_at ?? undefined
607+
});
608+
511609
const [tokensIn, tokensOut, sandboxHours, chatMessages] = await Promise.all([
512610
checkFeature({
513611
customerId: autumnCustomer.id ?? instance.clerkId,
@@ -607,15 +705,6 @@ export const createCheckoutSession = action({
607705
throw new Error(payload.error.message ?? 'Failed to create checkout session');
608706
}
609707
if (payload.data?.url) {
610-
await ctx.scheduler.runAfter(0, internal.analytics.trackEvent, {
611-
distinctId: instance.clerkId,
612-
event: AnalyticsEvents.SUBSCRIPTION_CREATED,
613-
properties: {
614-
instanceId: args.instanceId,
615-
plan: 'btca_pro',
616-
wasExistingCustomer: true
617-
}
618-
});
619708
return { url: payload.data.url };
620709
}
621710

0 commit comments

Comments
 (0)