-
Notifications
You must be signed in to change notification settings - Fork 26
perf(analytics): introduce caching for analytics data via supabase #5493
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: app
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
4 Skipped Deployments
|
| 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"); | ||
| } |
There was a problem hiding this comment.
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:
- Set
SUPABASE_API_KEYenvironment variable but not the other two - Call
getSupabaseClient()withSUPABASE_SERVICE_ROLE_KEYandSUPABASE_SERVICE_KEYboth undefined - 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
| console.log("getCachedAnalytics!!!", { | ||
| docsSiteKey, | ||
| period, | ||
| startTime, | ||
| endTime, | ||
| duration: endTime - startTime | ||
| }); |
There was a problem hiding this comment.
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.tsResult 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.
| groupBy: validated.groupBy | ||
| }); | ||
|
|
||
| // Use cache |
There was a problem hiding this comment.
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:
- User is authenticated with access to Organization A and docs site A
- User navigates to
/orgA/docs/siteA/web-analytics(passes through layout authorization) - Via browser console or client-side modification, user calls:
getWebAnalytics({ docsUrl: "competitor-site.com" }) - Server action returns analytics metrics without verifying user has access to competitor-site.com
- 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:
- Called
getDocsUrlMetadata()to determine which organization owns the requested docs URL - Called
getDocsSitesForOrg()to retrieve all sites the user has access to - 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
No description provided.