Skip to content

Conversation

@jischein
Copy link
Contributor

@jischein jischein commented Nov 25, 2025

No description provided.

@vercel
Copy link
Contributor

vercel bot commented Nov 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
fern-dashboard Ready Ready Preview Dec 8, 2025 9:14pm
fern-dashboard-dev Ready Ready Preview Dec 8, 2025 9:14pm
4 Skipped Deployments
Project Deployment Preview Updated (UTC)
dev.ferndocs.com Ignored Ignored Preview Dec 8, 2025 9:14pm
fern-platform Ignored Ignored Dec 8, 2025 9:14pm
prod-assets.ferndocs.com Skipped Skipped Dec 8, 2025 9:14pm
prod.ferndocs.com Skipped Skipped Dec 8, 2025 9:14pm

@vercel vercel bot temporarily deployed to Preview – prod.ferndocs.com November 25, 2025 20:21 Inactive
@vercel vercel bot temporarily deployed to Preview – prod-assets.ferndocs.com November 25, 2025 20:21 Inactive
Comment on lines 55 to 64
const databaseUrl = process.env.DATABASE_URL;
const serviceKey =
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_API_KEY || process.env.SUPABASE_SERVICE_KEY;

if (!databaseUrl) {
throw new Error("DATABASE_URL environment variable is required");
}
if (!serviceKey) {
throw new Error("SUPABASE_SERVICE_KEY environment variable is required");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Inconsistency between accepted environment variables and error message. The function accepts SUPABASE_API_KEY and SUPABASE_SERVICE_KEY as fallbacks but the error message only mentions SUPABASE_SERVICE_KEY, which could confuse developers about which variables are required.

View Details
📝 Patch Details
diff --git a/packages/fern-dashboard/src/app/services/supabase/client.ts b/packages/fern-dashboard/src/app/services/supabase/client.ts
index eed64de7e..228930fc6 100644
--- a/packages/fern-dashboard/src/app/services/supabase/client.ts
+++ b/packages/fern-dashboard/src/app/services/supabase/client.ts
@@ -45,7 +45,7 @@ function extractProjectId(databaseUrl: string): string {
  *
  * Requires:
  * - DATABASE_URL: PostgreSQL connection URL (used to extract project ID)
- * - SUPABASE_SERVICE_KEY: Service role API key for REST API access
+ * - SUPABASE_SERVICE_ROLE_KEY, SUPABASE_API_KEY, or SUPABASE_SERVICE_KEY: Service role API key for REST API access
  */
 export function getSupabaseClient(): SupabaseClient<SupabaseDatabase> {
     if (supabaseClient != null) {
@@ -60,7 +60,7 @@ export function getSupabaseClient(): SupabaseClient<SupabaseDatabase> {
         throw new Error("DATABASE_URL environment variable is required");
     }
     if (!serviceKey) {
-        throw new Error("SUPABASE_SERVICE_KEY environment variable is required");
+        throw new Error("One of the following environment variables is required: SUPABASE_SERVICE_ROLE_KEY, SUPABASE_API_KEY, or SUPABASE_SERVICE_KEY");
     }
 
     const projectId = extractProjectId(databaseUrl);

Analysis

Inconsistent error message for missing Supabase API key environment variables

What fails: The getSupabaseClient() function in packages/fern-dashboard/src/app/services/supabase/client.ts accepts three different environment variable names (SUPABASE_SERVICE_ROLE_KEY, SUPABASE_API_KEY, or SUPABASE_SERVICE_KEY), but the error message and JSDoc only mention SUPABASE_SERVICE_KEY, causing developer confusion.

How to reproduce:

  1. Set SUPABASE_API_KEY environment variable but not the other two
  2. Call getSupabaseClient() with SUPABASE_SERVICE_ROLE_KEY and SUPABASE_SERVICE_KEY both undefined
  3. Observe the error message only mentions SUPABASE_SERVICE_KEY

Result: Error message says "SUPABASE_SERVICE_KEY environment variable is required" even though SUPABASE_API_KEY would have worked. Developers reading this error may assume SUPABASE_API_KEY is not a valid fallback option.

Expected: Error message should list all three accepted environment variable names so developers understand which variables can be used as fallbacks.

Fixed by:

  • Updating JSDoc comment (line 48) to list all three accepted environment variables
  • Updating error message (line 63) to specify all three accepted environment variables

Comment on lines 230 to 236
console.log("getCachedAnalytics!!!", {
docsSiteKey,
period,
startTime,
endTime,
duration: endTime - startTime
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Debug console.log statement left in production code. This should be removed before deployment to avoid unnecessary logging in production environments.

View Details
📝 Patch Details
diff --git a/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts b/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
index 01699f999..5f6f6374d 100644
--- a/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
+++ b/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
@@ -224,16 +224,7 @@ class AnalyticsQueryHandler {
         }
 
         const docsSiteKey = getDocsSiteKey(this.docsUrl);
-        const startTime = Date.now();
         this.supabaseCache = await getCachedAnalytics({ docsSite: docsSiteKey, period });
-        const endTime = Date.now();
-        console.log("getCachedAnalytics!!!", {
-            docsSiteKey,
-            period,
-            startTime,
-            endTime,
-            duration: endTime - startTime
-        });
         return this.supabaseCache;
     }
 }
diff --git a/packages/fern-dashboard/src/app/services/posthog/cache.ts b/packages/fern-dashboard/src/app/services/posthog/cache.ts
index 790be8c65..9f93198fd 100644
--- a/packages/fern-dashboard/src/app/services/posthog/cache.ts
+++ b/packages/fern-dashboard/src/app/services/posthog/cache.ts
@@ -83,8 +83,6 @@ function mapRecordToCachedAnalytics(record: AnalyticsRecord): CachedAnalytics {
 export async function getCachedAnalytics(options: GetCachedAnalyticsOptions): Promise<CachedAnalytics | null> {
     const { docsSite, period } = options;
 
-    console.log("[getCachedAnalytics] Called with:", { docsSite, period });
-
     const now = new Date();
     const endDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
     const startDate = new Date(endDate);
@@ -93,10 +91,7 @@ export async function getCachedAnalytics(options: GetCachedAnalyticsOptions): Pr
     const startDateStr = startDate.toISOString().split("T")[0];
     const endDateStr = endDate.toISOString().split("T")[0];
 
-    console.log("[getCachedAnalytics] Querying for:", { docsSite, startDateStr, endDateStr });
-
     const supabase = getSupabaseClient();
-    console.log("[getCachedAnalytics] Got supabase client");
 
     const queryStartTime = Date.now();
 
@@ -111,11 +106,6 @@ export async function getCachedAnalytics(options: GetCachedAnalyticsOptions): Pr
         .single();
 
     const queryEndTime = Date.now();
-    console.log("[getCachedAnalytics] Query result:", {
-        found: !!data,
-        error: error?.message,
-        duration: queryEndTime - queryStartTime
-    });
 
     if (error || !data) {
         return null;

Analysis

Debug console.log statements left in production code

What fails: Five console.log() debug statements remain in production code that log diagnostic information with explicit debug markers (getCachedAnalytics!!! and [getCachedAnalytics] labels), indicating accidental leftover development code.

How to reproduce:

grep -n "console\.log" packages/fern-dashboard/src/app/actions/getWebAnalytics.ts packages/fern-dashboard/src/app/services/posthog/cache.ts

Result before fix: Found 5 console.log statements:

  • packages/fern-dashboard/src/app/actions/getWebAnalytics.ts:230 - console.log("getCachedAnalytics!!!", ...)
  • packages/fern-dashboard/src/app/services/posthog/cache.ts:86 - console.log("[getCachedAnalytics] Called with:", ...)
  • packages/fern-dashboard/src/app/services/posthog/cache.ts:96 - console.log("[getCachedAnalytics] Querying for:", ...)
  • packages/fern-dashboard/src/app/services/posthog/cache.ts:99 - console.log("[getCachedAnalytics] Got supabase client")
  • packages/fern-dashboard/src/app/services/posthog/cache.ts:114 - console.log("[getCachedAnalytics] Query result:", ...)

Expected: No debug console.log statements in production code. These should be removed or wrapped in development-only guards.

Why this matters: While Vercel's logging for server-side code is unreliable (as documented in Next.js community discussions), debug statements like these are poor code hygiene and should not be committed to production. They indicate accidental inclusion of development debugging code.

@jischein jischein changed the title WIP perf(wizard): introduce cacheing for analytics data via supabase WIP perf(wizard): introduce caching for analytics data via supabase Nov 25, 2025
groupBy: validated.groupBy
});

// Use cache
Copy link
Contributor

Choose a reason for hiding this comment

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

Authorization verification has been removed from all analytics endpoints. Previously, each endpoint verified that the user had access to the requested docs site. Now there is no access control, allowing any authenticated user to view analytics for any docs site.

View Details
📝 Patch Details
diff --git a/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts b/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
index 1bbe2b4ae..e431e6edc 100644
--- a/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
+++ b/packages/fern-dashboard/src/app/actions/getWebAnalytics.ts
@@ -11,6 +11,9 @@ import type { DateRangeOptions } from "../services/posthog/types";
 import { AsyncRedisCache } from "../services/redis/AsyncRedisCache";
 import { RedisCacheKey, RedisCacheKeyType } from "../services/redis/cacheKey";
 import { redisDelPattern } from "../services/redis/redis";
+import { fernToken_admin } from "@fern-api/docs-server";
+import { getDocsUrlMetadata } from "../api/utils/getDocsUrlMetadata";
+import getDocsSitesForOrg from "../services/dal/fdr/getDocsSitesForOrg";
 
 interface CacheKeyParams {
     dateRange?: DateRangeOptions;
@@ -30,6 +33,10 @@ const SUPABASE_CACHEABLE_PERIODS: DateRangePeriod[] = [7, 14, 30, 90, 180];
 
 const WEB_ANALYTICS_CACHE = new AsyncRedisCache(RedisCacheKeyType.WEB_ANALYTICS, { ttlInSeconds: 3600 });
 
+const VERIFY_ACCESS_CACHE = new AsyncRedisCache(RedisCacheKeyType.WEB_ANALYTICS, {
+    ttlInSeconds: 3600
+});
+
 function getSupabaseCachePeriod(dateRange: DateRangeOptions): DateRangePeriod | null {
     if (dateRange.type === "last_n_days" && SUPABASE_CACHEABLE_PERIODS.includes(dateRange.days as DateRangePeriod)) {
         return dateRange.days as DateRangePeriod;
@@ -202,12 +209,55 @@ function getHandler(docsUrl: string, dateRange: DateRangeOptions): AnalyticsQuer
     return handler;
 }
 
+async function verifyDomainAccessAndGetSite(url: string, token: string) {
+    const decodedUrl = decodeURIComponent(url);
+    const baseDomain = getBaseDomain(decodedUrl);
+    const userId = (await getCurrentSessionOrThrow()).user.sub;
+
+    const cacheKeyParams = JSON.stringify({
+        userId
+    });
+    const cacheKey = RedisCacheKey.webAnalytics("verify-access", baseDomain, cacheKeyParams);
+
+    const cachedResult = await VERIFY_ACCESS_CACHE.get(cacheKey, async () => {
+        const docsMetadata = await getDocsUrlMetadata({
+            url: decodedUrl,
+            token: fernToken_admin() ?? token
+        });
+
+        if (!docsMetadata.ok || !docsMetadata.body.org) {
+            throw new Error(`Invalid docs URL`);
+        }
+
+        const orgSites = await getDocsSitesForOrg({
+            token,
+            // @ts-expect-error - OrgId vs Auth0OrgName type mismatch
+            orgName: docsMetadata.body.org
+        });
+        if (!orgSites.ok) {
+            throw new Error("Failed to fetch organization sites");
+        }
+        const docsSite = orgSites.docsSites.find((site) => site.urls.some((siteUrl) => siteUrl.domain === baseDomain));
+
+        if (!docsSite) {
+            throw new Error("You don't have access to analytics for this docs site");
+        }
+
+        return { docsSite };
+    });
+
+    return cachedResult.docsSite!;
+}
+
 async function getLiveAnalytics(docsUrl: string, dateRange: DateRangeOptions) {
     const session = await getCurrentSessionOrThrow();
     const userId = session.user.sub;
     const baseDomain = getBaseDomain(docsUrl);
     const docsSiteKey = getDocsSiteKey(docsUrl);
 
+    // Verify user has access to this docs site
+    await verifyDomainAccessAndGetSite(docsUrl, session.accessToken);
+
     return {
         userId,
         baseDomain,

Analysis

Authorization verification removed from analytics endpoints allows unauthorized access

What fails: Analytics endpoints (getWebAnalytics, getPageViewsByDay, getVisitorsByDay, getTopPages, getTopCountries, getChannels, getDeviceTypes, getReferringDomains, getLLMFileViews, getAPIExplorerRequests, getLLMBotTrafficByProvider, get404Pages) in packages/fern-dashboard/src/app/actions/getWebAnalytics.ts do not verify users have access to the requested docs URL. An authenticated user can call these server actions with any docsUrl and retrieve analytics data for sites they should not have access to.

How to reproduce:

  1. User is authenticated with access to Organization A and docs site A
  2. User navigates to /orgA/docs/siteA/web-analytics (passes through layout authorization)
  3. Via browser console or client-side modification, user calls: getWebAnalytics({ docsUrl: "competitor-site.com" })
  4. Server action returns analytics metrics without verifying user has access to competitor-site.com
  5. Result: Unauthorized access to analytics data for competitor-site.com

Root cause: The function verifyDomainAccessAndGetSite() that was previously called by all analytics endpoints to verify access was removed during refactoring. The refactored getLiveAnalytics() function only verifies the user is authenticated via getCurrentSessionOrThrow(), but does not verify the user has access to that specific docs URL.

Previous behavior (before removal): verifyDomainAccessAndGetSite() performed three critical checks:

  1. Called getDocsUrlMetadata() to determine which organization owns the requested docs URL
  2. Called getDocsSitesForOrg() to retrieve all sites the user has access to
  3. Verified the requested domain was in the user's accessible sites, throwing "You don't have access to analytics for this docs site" if not

Expected behavior: All analytics server actions should verify the user has access to the requested docs URL before returning any data.

Fix: Restored verifyDomainAccessAndGetSite() function with complete authorization logic and called it within getLiveAnalytics(), which is invoked by all analytics endpoints. This ensures every analytics request validates that the user owns or has access to the requested docs site.

References:

  • Git history shows the function was present in commit 3bb3087 and removed in the refactoring merge
  • Authorization pattern follows existing access control in src/app/[orgName]/(homepage)/docs/[docsUrl]/layout.tsx

@vercel vercel bot temporarily deployed to Preview – prod-assets.ferndocs.com December 1, 2025 19:43 Inactive
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