diff --git a/.changeset/dry-singers-pull.md b/.changeset/dry-singers-pull.md
new file mode 100644
index 0000000000..cb633bcc8e
--- /dev/null
+++ b/.changeset/dry-singers-pull.md
@@ -0,0 +1,5 @@
+---
+"adcontextprotocol": patch
+---
+
+Fix admin dashboard null reference errors, route ordering for view-counts endpoint, and capture early JavaScript errors before PostHog loads.
diff --git a/server/public/admin.html b/server/public/admin.html
index 591a729d55..e430365383 100644
--- a/server/public/admin.html
+++ b/server/public/admin.html
@@ -828,8 +828,6 @@
Dashboard
// FUNNEL
document.getElementById('totalOrgs').textContent = formatNumber(stats.total_orgs);
document.getElementById('prospects').textContent = formatNumber(stats.prospects);
- document.getElementById('engagedProspects').textContent = formatNumber(stats.engaged_prospects);
- document.getElementById('trials').textContent = formatNumber(stats.trials);
document.getElementById('payingOrgs').textContent = formatNumber(stats.paying_orgs);
document.getElementById('totalUsers').textContent = formatNumber(stats.total_users);
document.getElementById('activeSubscriptions').textContent = formatNumber(stats.active_subscriptions);
diff --git a/server/public/posthog-init.js b/server/public/posthog-init.js
index c99a6bde86..379f75884a 100644
--- a/server/public/posthog-init.js
+++ b/server/public/posthog-init.js
@@ -93,4 +93,25 @@
is_admin: user.isAdmin,
});
}
+
+ // Flush any errors that occurred before PostHog loaded
+ if (window.__earlyErrors && window.__earlyErrors.length) {
+ window.__earlyErrors.forEach(function(err) {
+ if (err.type === 'unhandledrejection') {
+ posthog.captureException(err.reason || new Error('Unhandled rejection'), {
+ type: 'unhandledrejection',
+ early_capture: true,
+ });
+ } else {
+ posthog.captureException(err.error || new Error(err.message || 'Unknown error'), {
+ source: err.source,
+ lineno: err.lineno,
+ colno: err.colno,
+ early_capture: true,
+ });
+ }
+ });
+ window.__earlyErrors = [];
+ window.onerror = null; // Clear early capture handler
+ }
})();
diff --git a/server/src/routes/admin/accounts.ts b/server/src/routes/admin/accounts.ts
index 740055f659..e10c977379 100644
--- a/server/src/routes/admin/accounts.ts
+++ b/server/src/routes/admin/accounts.ts
@@ -126,6 +126,153 @@ export function setupAccountRoutes(
}
);
+ // GET /api/admin/accounts/view-counts - Get counts for each view tab
+ // NOTE: Must be registered BEFORE /accounts/:orgId to avoid matching "view-counts" as an orgId
+ apiRouter.get(
+ "/accounts/view-counts",
+ requireAuth,
+ requireAdmin,
+ async (req, res) => {
+ try {
+ const pool = getPool();
+ const currentUserId = req.user?.id;
+
+ const [
+ needsAttention,
+ newInsights,
+ hot,
+ newProspects,
+ goingCold,
+ myAccounts,
+ renewals,
+ members,
+ disqualified,
+ ] = await Promise.all([
+ // Needs attention - all accounts needing action
+ pool.query(`
+ SELECT COUNT(DISTINCT o.workos_organization_id) as count
+ FROM organizations o
+ LEFT JOIN org_activities na ON na.organization_id = o.workos_organization_id
+ AND na.is_next_step = TRUE
+ AND na.next_step_completed_at IS NULL
+ AND (na.next_step_due_date IS NULL OR na.next_step_due_date <= NOW() + INTERVAL '7 days')
+ LEFT JOIN org_invoices oi ON oi.workos_organization_id = o.workos_organization_id
+ AND oi.status IN ('draft', 'open')
+ WHERE COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ AND (
+ na.id IS NOT NULL
+ OR oi.stripe_invoice_id IS NOT NULL
+ OR (
+ COALESCE(o.engagement_score, 0) >= 50
+ AND NOT EXISTS (
+ SELECT 1 FROM org_stakeholders os WHERE os.organization_id = o.workos_organization_id
+ )
+ )
+ )
+ `),
+
+ // New insights - prospects with recent Slack activity
+ pool.query(`
+ SELECT COUNT(DISTINCT o.workos_organization_id) as count
+ FROM organizations o
+ WHERE EXISTS (
+ SELECT 1 FROM organization_memberships om
+ JOIN slack_user_mappings sm ON sm.workos_user_id = om.workos_user_id
+ WHERE om.workos_organization_id = o.workos_organization_id
+ AND sm.last_slack_activity_at >= NOW() - INTERVAL '7 days'
+ )
+ AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
+ `),
+
+ // Hot prospects
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
+ AND COALESCE(o.engagement_score, 0) >= 50
+ AND o.interest_level IN ('high', 'very_high')
+ AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ `),
+
+ // New prospects - recently created non-members
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE o.created_at >= NOW() - INTERVAL '14 days'
+ AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
+ `),
+
+ // Going cold
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE o.last_activity_at IS NOT NULL
+ AND o.last_activity_at < NOW() - INTERVAL '30 days'
+ AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
+ `),
+
+ // My accounts
+ currentUserId
+ ? pool.query(
+ `
+ SELECT COUNT(DISTINCT o.workos_organization_id) as count
+ FROM organizations o
+ INNER JOIN org_stakeholders os ON os.organization_id = o.workos_organization_id AND os.user_id = $1
+ WHERE COALESCE(o.prospect_status, 'prospect') != 'disqualified'
+ `,
+ [currentUserId]
+ )
+ : Promise.resolve({ rows: [{ count: 0 }] }),
+
+ // Renewals
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE o.subscription_status = 'active'
+ AND o.subscription_current_period_end IS NOT NULL
+ AND o.subscription_current_period_end <= NOW() + INTERVAL '60 days'
+ AND o.subscription_current_period_end > NOW()
+ `),
+
+ // Members
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE o.subscription_status = 'active'
+ `),
+
+ // Disqualified
+ pool.query(`
+ SELECT COUNT(*) as count
+ FROM organizations o
+ WHERE o.prospect_status = 'disqualified'
+ `),
+ ]);
+
+ res.json({
+ needs_attention: parseInt(needsAttention.rows[0].count),
+ new_insights: parseInt(newInsights.rows[0].count),
+ hot: parseInt(hot.rows[0].count),
+ new_prospects: parseInt(newProspects.rows[0].count),
+ going_cold: parseInt(goingCold.rows[0].count),
+ my_accounts: parseInt(myAccounts.rows[0].count),
+ renewals: parseInt(renewals.rows[0].count),
+ members: parseInt(members.rows[0].count),
+ disqualified: parseInt(disqualified.rows[0].count),
+ });
+ } catch (error) {
+ logger.error({ err: error }, "Error fetching view counts");
+ res.status(500).json({
+ error: "Internal server error",
+ message: "Unable to fetch view counts",
+ });
+ }
+ }
+ );
+
// GET /api/admin/accounts/:orgId - Unified account detail
apiRouter.get(
"/accounts/:orgId",
@@ -909,152 +1056,6 @@ export function setupAccountRoutes(
}
});
- // GET /api/admin/accounts/view-counts - Get counts for each view tab
- apiRouter.get(
- "/accounts/view-counts",
- requireAuth,
- requireAdmin,
- async (req, res) => {
- try {
- const pool = getPool();
- const currentUserId = req.user?.id;
-
- const [
- needsAttention,
- newInsights,
- hot,
- newProspects,
- goingCold,
- myAccounts,
- renewals,
- members,
- disqualified,
- ] = await Promise.all([
- // Needs attention - all accounts needing action
- pool.query(`
- SELECT COUNT(DISTINCT o.workos_organization_id) as count
- FROM organizations o
- LEFT JOIN org_activities na ON na.organization_id = o.workos_organization_id
- AND na.is_next_step = TRUE
- AND na.next_step_completed_at IS NULL
- AND (na.next_step_due_date IS NULL OR na.next_step_due_date <= NOW() + INTERVAL '7 days')
- LEFT JOIN org_invoices oi ON oi.workos_organization_id = o.workos_organization_id
- AND oi.status IN ('draft', 'open')
- WHERE COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- AND (
- na.id IS NOT NULL
- OR oi.stripe_invoice_id IS NOT NULL
- OR (
- COALESCE(o.engagement_score, 0) >= 50
- AND NOT EXISTS (
- SELECT 1 FROM org_stakeholders os WHERE os.organization_id = o.workos_organization_id
- )
- )
- )
- `),
-
- // New insights - prospects with recent Slack activity
- pool.query(`
- SELECT COUNT(DISTINCT o.workos_organization_id) as count
- FROM organizations o
- WHERE EXISTS (
- SELECT 1 FROM organization_memberships om
- JOIN slack_user_mappings sm ON sm.workos_user_id = om.workos_user_id
- WHERE om.workos_organization_id = o.workos_organization_id
- AND sm.last_slack_activity_at >= NOW() - INTERVAL '7 days'
- )
- AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
- `),
-
- // Hot prospects
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
- AND COALESCE(o.engagement_score, 0) >= 50
- AND o.interest_level IN ('high', 'very_high')
- AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- `),
-
- // New prospects - recently created non-members
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE o.created_at >= NOW() - INTERVAL '14 days'
- AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
- `),
-
- // Going cold
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE o.last_activity_at IS NOT NULL
- AND o.last_activity_at < NOW() - INTERVAL '30 days'
- AND COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- AND (o.subscription_status IS NULL OR o.subscription_status NOT IN ('active', 'trialing'))
- `),
-
- // My accounts
- currentUserId
- ? pool.query(
- `
- SELECT COUNT(DISTINCT o.workos_organization_id) as count
- FROM organizations o
- INNER JOIN org_stakeholders os ON os.organization_id = o.workos_organization_id AND os.user_id = $1
- WHERE COALESCE(o.prospect_status, 'prospect') != 'disqualified'
- `,
- [currentUserId]
- )
- : Promise.resolve({ rows: [{ count: 0 }] }),
-
- // Renewals
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE o.subscription_status = 'active'
- AND o.subscription_current_period_end IS NOT NULL
- AND o.subscription_current_period_end <= NOW() + INTERVAL '60 days'
- AND o.subscription_current_period_end > NOW()
- `),
-
- // Members
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE o.subscription_status = 'active'
- `),
-
- // Disqualified
- pool.query(`
- SELECT COUNT(*) as count
- FROM organizations o
- WHERE o.prospect_status = 'disqualified'
- `),
- ]);
-
- res.json({
- needs_attention: parseInt(needsAttention.rows[0].count),
- new_insights: parseInt(newInsights.rows[0].count),
- hot: parseInt(hot.rows[0].count),
- new_prospects: parseInt(newProspects.rows[0].count),
- going_cold: parseInt(goingCold.rows[0].count),
- my_accounts: parseInt(myAccounts.rows[0].count),
- renewals: parseInt(renewals.rows[0].count),
- members: parseInt(members.rows[0].count),
- disqualified: parseInt(disqualified.rows[0].count),
- });
- } catch (error) {
- logger.error({ err: error }, "Error fetching view counts");
- res.status(500).json({
- error: "Internal server error",
- message: "Unable to fetch view counts",
- });
- }
- }
- );
-
/**
* Convert account type between personal and team
*
diff --git a/server/src/utils/html-config.ts b/server/src/utils/html-config.ts
index 8da68f2427..34323dda82 100644
--- a/server/src/utils/html-config.ts
+++ b/server/src/utils/html-config.ts
@@ -70,6 +70,18 @@ export function getAppConfigScript(user?: AppUser | null): string {
return ``;
}
+/**
+ * Inline script that buffers errors before PostHog loads.
+ * Must run synchronously before any other scripts.
+ */
+const EARLY_ERROR_BUFFER_SCRIPT = ``;
+
/**
* Inject app config into HTML string.
* Inserts before or before found.
@@ -78,12 +90,12 @@ export function getAppConfigScript(user?: AppUser | null): string {
export function injectConfigIntoHtml(html: string, user?: AppUser | null): string {
const configScript = getAppConfigScript(user);
- // Add PostHog script if API key is configured
- const posthogScript = POSTHOG_API_KEY
- ? ``
+ // Add early error buffer (sync) and PostHog script (deferred) if API key is configured
+ const posthogScripts = POSTHOG_API_KEY
+ ? `${EARLY_ERROR_BUFFER_SCRIPT}\n`
: '';
- const injectedScripts = `${configScript}\n${posthogScript}`;
+ const injectedScripts = `${configScript}\n${posthogScripts}`;
if (html.includes("")) {
return html.replace("", `${injectedScripts}\n`);