From e7da2f41f19122168cdb1fc9d1dd28efaafb83f7 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 12 Dec 2025 11:58:17 -0500 Subject: [PATCH 1/4] Fix analytics dashboard zeroes by syncing subscription data during backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The analytics dashboard was showing zero revenue and MRR because the subscription_amount field on organizations was not being populated. The backfill endpoint now syncs active subscription data from Stripe for each linked customer, populating subscription_amount, interval, and current_period_end needed for MRR calculation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/public/admin-analytics.html | 3 +- server/src/http.ts | 79 ++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/server/public/admin-analytics.html b/server/public/admin-analytics.html index 352b6eb95a..bd2d627e19 100644 --- a/server/public/admin-analytics.html +++ b/server/public/admin-analytics.html @@ -406,7 +406,8 @@

Revenue by Product

const result = await response.json(); - messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.`; + const subsSyncedMsg = result.subscriptions_synced ? ` Synced ${result.subscriptions_synced} subscriptions.` : ''; + messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.${subsSyncedMsg}`; messageEl.style.display = 'block'; messageEl.className = 'success-message'; diff --git a/server/src/http.ts b/server/src/http.ts index 685a575155..156b2c1cb9 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -2652,11 +2652,89 @@ export class HTTPServer { } } + // Sync subscription data to organizations for MRR calculation + // This populates subscription_amount, subscription_interval, subscription_current_period_end + let subscriptionsSynced = 0; + if (stripe) { + for (const [customerId, workosOrgId] of customerOrgMap) { + try { + // Get customer with subscriptions + const customer = await stripe.customers.retrieve(customerId, { + expand: ['subscriptions'], + }); + + if ((customer as Stripe.Customer).deleted) { + continue; + } + + const subscriptions = (customer as Stripe.Customer).subscriptions; + if (!subscriptions || subscriptions.data.length === 0) { + continue; + } + + // Get the first active subscription + const subscription = subscriptions.data[0]; + if (!subscription || !['active', 'trialing', 'past_due'].includes(subscription.status)) { + continue; + } + + // Get full subscription details + const fullSubscription = await stripe.subscriptions.retrieve(subscription.id, { + expand: ['items.data.price.product'], + }); + + // Get primary subscription item + const primaryItem = fullSubscription.items.data[0]; + if (!primaryItem) { + continue; + } + + const price = primaryItem.price; + const product = price?.product as Stripe.Product | undefined; + const amount = price?.unit_amount ?? 0; + const interval = price?.recurring?.interval ?? null; + + // Update organization with subscription details + await pool.query( + `UPDATE organizations + SET subscription_amount = $1, + subscription_interval = $2, + subscription_currency = $3, + subscription_current_period_end = $4, + subscription_canceled_at = $5, + subscription_product_id = $6, + subscription_product_name = $7, + subscription_price_id = $8, + updated_at = NOW() + WHERE workos_organization_id = $9`, + [ + amount, + interval, + price?.currency || 'usd', + fullSubscription.current_period_end ? new Date(fullSubscription.current_period_end * 1000) : null, + fullSubscription.canceled_at ? new Date(fullSubscription.canceled_at * 1000) : null, + product?.id || null, + product?.name || null, + price?.id || null, + workosOrgId, + ] + ); + + subscriptionsSynced++; + logger.debug({ workosOrgId, customerId, amount, interval }, 'Synced subscription data'); + } catch (subError) { + logger.error({ err: subError, customerId, workosOrgId }, 'Failed to sync subscription for customer'); + // Continue with other customers + } + } + } + logger.info({ invoices: invoices.length, refunds: refunds.length, imported, skipped, + subscriptionsSynced, }, 'Revenue backfill completed'); res.json({ @@ -2666,6 +2744,7 @@ export class HTTPServer { refunds_found: refunds.length, imported, skipped, + subscriptions_synced: subscriptionsSynced, }); } catch (error) { logger.error({ err: error }, 'Error during revenue backfill'); From bc30eed5b2aecd0bc9b5ccad976f667f6b9caa30 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 12 Dec 2025 12:02:58 -0500 Subject: [PATCH 2/4] Add changeset for analytics fix --- .changeset/fix-analytics-zeroes.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-analytics-zeroes.md diff --git a/.changeset/fix-analytics-zeroes.md b/.changeset/fix-analytics-zeroes.md new file mode 100644 index 0000000000..6720ea99c4 --- /dev/null +++ b/.changeset/fix-analytics-zeroes.md @@ -0,0 +1,5 @@ +--- +"adcontextprotocol": patch +--- + +Fix analytics dashboard showing zeroes by syncing subscription data during backfill From 69e14bec0422115dc669197374d13a0d5832f79e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 12 Dec 2025 12:05:31 -0500 Subject: [PATCH 3/4] Optimize Stripe API calls and track failed syncs --- server/public/admin-analytics.html | 3 ++- server/src/http.ts | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/public/admin-analytics.html b/server/public/admin-analytics.html index bd2d627e19..80716c905e 100644 --- a/server/public/admin-analytics.html +++ b/server/public/admin-analytics.html @@ -407,7 +407,8 @@

Revenue by Product

const result = await response.json(); const subsSyncedMsg = result.subscriptions_synced ? ` Synced ${result.subscriptions_synced} subscriptions.` : ''; - messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.${subsSyncedMsg}`; + const subsFailedMsg = result.subscriptions_failed ? ` (${result.subscriptions_failed} failed)` : ''; + messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.${subsSyncedMsg}${subsFailedMsg}`; messageEl.style.display = 'block'; messageEl.className = 'success-message'; diff --git a/server/src/http.ts b/server/src/http.ts index 156b2c1cb9..dc43d4c1e0 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -2655,15 +2655,16 @@ export class HTTPServer { // Sync subscription data to organizations for MRR calculation // This populates subscription_amount, subscription_interval, subscription_current_period_end let subscriptionsSynced = 0; + let subscriptionsFailed = 0; if (stripe) { for (const [customerId, workosOrgId] of customerOrgMap) { try { - // Get customer with subscriptions + // Get customer with subscriptions and expanded price/product data in single API call const customer = await stripe.customers.retrieve(customerId, { - expand: ['subscriptions'], + expand: ['subscriptions.data.items.data.price.product'], }); - if ((customer as Stripe.Customer).deleted) { + if ('deleted' in customer && customer.deleted) { continue; } @@ -2672,19 +2673,14 @@ export class HTTPServer { continue; } - // Get the first active subscription + // Get the first active subscription (already has expanded items) const subscription = subscriptions.data[0]; if (!subscription || !['active', 'trialing', 'past_due'].includes(subscription.status)) { continue; } - // Get full subscription details - const fullSubscription = await stripe.subscriptions.retrieve(subscription.id, { - expand: ['items.data.price.product'], - }); - - // Get primary subscription item - const primaryItem = fullSubscription.items.data[0]; + // Get primary subscription item directly from expanded data + const primaryItem = subscription.items.data[0]; if (!primaryItem) { continue; } @@ -2711,8 +2707,8 @@ export class HTTPServer { amount, interval, price?.currency || 'usd', - fullSubscription.current_period_end ? new Date(fullSubscription.current_period_end * 1000) : null, - fullSubscription.canceled_at ? new Date(fullSubscription.canceled_at * 1000) : null, + subscription.current_period_end ? new Date(subscription.current_period_end * 1000) : null, + subscription.canceled_at ? new Date(subscription.canceled_at * 1000) : null, product?.id || null, product?.name || null, price?.id || null, @@ -2723,6 +2719,7 @@ export class HTTPServer { subscriptionsSynced++; logger.debug({ workosOrgId, customerId, amount, interval }, 'Synced subscription data'); } catch (subError) { + subscriptionsFailed++; logger.error({ err: subError, customerId, workosOrgId }, 'Failed to sync subscription for customer'); // Continue with other customers } @@ -2735,6 +2732,7 @@ export class HTTPServer { imported, skipped, subscriptionsSynced, + subscriptionsFailed, }, 'Revenue backfill completed'); res.json({ @@ -2745,6 +2743,7 @@ export class HTTPServer { imported, skipped, subscriptions_synced: subscriptionsSynced, + subscriptions_failed: subscriptionsFailed, }); } catch (error) { logger.error({ err: error }, 'Error during revenue backfill'); From 5067a09d85bb7b7e1e9d79cdc5bab1596ebe5895 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Fri, 12 Dec 2025 14:52:52 -0500 Subject: [PATCH 4/4] Change to empty changeset (internal admin fix) --- .changeset/fix-analytics-zeroes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.changeset/fix-analytics-zeroes.md b/.changeset/fix-analytics-zeroes.md index 6720ea99c4..b52b6e6cf3 100644 --- a/.changeset/fix-analytics-zeroes.md +++ b/.changeset/fix-analytics-zeroes.md @@ -1,5 +1,4 @@ --- -"adcontextprotocol": patch --- -Fix analytics dashboard showing zeroes by syncing subscription data during backfill +Fix analytics dashboard showing zeroes by syncing subscription data during backfill (internal admin change)