Skip to content

Commit bea3cdc

Browse files
authored
feat(analytics): Introduce domain toggle (#5773)
1 parent baea60b commit bea3cdc

File tree

8 files changed

+228
-39
lines changed

8 files changed

+228
-39
lines changed

packages/fern-dashboard/scripts/list-production-domains.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#!/usr/bin/env npx tsx
22
/**
3-
* List all production domains from the KV store
3+
* List all production domains from the KV store or FDR
44
*
55
* Usage:
66
* npx tsx scripts/list-production-domains.ts
77
* npx tsx scripts/list-production-domains.ts --include-previews
8-
* npx tsx scripts/list-production-domains.ts --json
8+
* npx tsx scripts/list-production-domains.ts --force-fdr
9+
* npx tsx scripts/list-production-domains.ts --force-fdr --json
910
*/
10-
1111
import { config } from "dotenv";
1212
import { dirname, resolve } from "path";
1313
import { fileURLToPath } from "url";
@@ -24,6 +24,7 @@ async function main() {
2424
const { values } = parseArgs({
2525
options: {
2626
"include-previews": { type: "boolean", default: false },
27+
"force-fdr": { type: "boolean", default: false },
2728
json: { type: "boolean", default: false },
2829
help: { type: "boolean", short: "h" }
2930
},
@@ -39,6 +40,7 @@ Usage:
3940
4041
Options:
4142
--include-previews Include Fern-hosted preview domains (*.docs.buildwithfern.com)
43+
--force-fdr Force fetching from FDR instead of KV store
4244
--json Output as JSON
4345
-h, --help Show this help message
4446
`);
@@ -49,22 +51,29 @@ Options:
4951
const hasKV = process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN && process.env.NEXT_PUBLIC_CDN_URI;
5052
const hasFDR = (process.env.FDR_SERVER_URL || process.env.NEXT_PUBLIC_FDR_ORIGIN) && process.env.FERN_TOKEN;
5153

54+
// If forcing FDR, require FDR credentials
55+
if (values["force-fdr"] && !hasFDR) {
56+
console.error(
57+
"Error: --force-fdr requires FDR credentials (FDR_SERVER_URL/NEXT_PUBLIC_FDR_ORIGIN, FERN_TOKEN)"
58+
);
59+
process.exit(1);
60+
}
61+
5262
if (!hasKV && !hasFDR) {
5363
console.error("Error: Either KV credentials (KV_REST_API_URL, KV_REST_API_TOKEN, NEXT_PUBLIC_CDN_URI)");
5464
console.error(" or FDR credentials (FDR_SERVER_URL/NEXT_PUBLIC_FDR_ORIGIN, FERN_TOKEN) are required");
5565
process.exit(1);
5666
}
5767

58-
console.log(`Using ${hasKV ? "KV store" : "FDR"} to fetch domains...\n`);
68+
const source = values["force-fdr"] ? "FDR (forced)" : hasKV ? "KV store" : "FDR";
69+
console.log(`Using ${source} to fetch domains...\n`);
5970

60-
const { getAllProductionDomains, getAllDomainsIncludingPreviews } = await import(
61-
"../src/app/services/analyticsCron/getAllProductionDomains"
62-
);
71+
const getAllProductionDomainsModule = await import("../src/app/services/analyticsCron/getAllProductionDomains");
6372

6473
try {
6574
if (values["include-previews"]) {
75+
const { getAllDomainsIncludingPreviews } = getAllProductionDomainsModule;
6676
const domains = await getAllDomainsIncludingPreviews();
67-
6877
if (values.json) {
6978
console.log(JSON.stringify(domains, null, 2));
7079
} else {
@@ -74,7 +83,10 @@ Options:
7483
}
7584
}
7685
} else {
77-
const domains = await getAllProductionDomains();
86+
// Use FDR directly if --force-fdr is set, otherwise use the smart function
87+
const domains = values["force-fdr"]
88+
? await getAllProductionDomainsModule.getAllProductionDomainsFromFDR()
89+
: await getAllProductionDomainsModule.getAllProductionDomains();
7890

7991
if (values.json) {
8092
console.log(JSON.stringify(domains, null, 2));

packages/fern-dashboard/src/app/[orgName]/(homepage)/docs/[docsUrl]/web-analytics/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ export default async function Page(props: { params: Promise<{ orgName: Auth0OrgN
77
const params = await props.params;
88
await getAuthenticatedSessionOrRedirect(params.orgName);
99

10-
return <WebAnalyticsPage docsUrl={params.docsUrl as DocsUrl} />;
10+
return <WebAnalyticsPage docsUrl={params.docsUrl as DocsUrl} orgName={params.orgName} />;
1111
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use server";
2+
3+
import type { FdrAPI } from "@fern-api/fdr-sdk/client/types";
4+
import { getDocsSiteUrl } from "@/utils/getDocsSiteUrl";
5+
import { parseDocsUrlParam } from "@/utils/parseDocsUrlParam";
6+
import { getCurrentSessionOrThrow } from "../services/auth0/getCurrentSession";
7+
import type { Auth0OrgName } from "../services/auth0/types";
8+
import getDocsSitesForOrg from "../services/dal/fdr/getDocsSitesForOrg";
9+
10+
export async function getDocsSiteDomains(
11+
docsUrl: string,
12+
orgName: Auth0OrgName
13+
): Promise<FdrAPI.dashboard.DocsSiteUrl[]> {
14+
const session = await getCurrentSessionOrThrow();
15+
16+
const response = await getDocsSitesForOrg({
17+
orgName,
18+
token: session.accessToken
19+
});
20+
21+
if (!response.ok) {
22+
throw new Error(`Failed to get docs sites: ${response.error.type}`);
23+
}
24+
25+
const parsedDocsUrl = parseDocsUrlParam({ docsUrl });
26+
const currentDocsSite = response.docsSites.find((site) => getDocsSiteUrl(site) === parsedDocsUrl);
27+
28+
if (!currentDocsSite) {
29+
throw new Error(`Docs site not found: ${docsUrl}`);
30+
}
31+
32+
return currentDocsSite.urls;
33+
}

packages/fern-dashboard/src/app/actions/getWebAnalytics.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ const GetWebAnalyticsSchema = z.object({
5959
])
6060
.optional(),
6161
includeInternal: z.boolean().optional(),
62-
groupBy: z.number().optional()
62+
groupBy: z.number().optional(),
63+
selectedDomain: z.string().optional()
6364
});
6465

6566
const TableRequestSchema = GetWebAnalyticsSchema.extend({
@@ -144,10 +145,12 @@ class AnalyticsQueryHandler {
144145
private supabaseCache: CachedAnalytics | null | undefined = undefined;
145146
private docsUrl: string;
146147
private dateRange: DateRangeOptions;
148+
private selectedDomain: string | undefined;
147149

148-
constructor(docsUrl: string, dateRange: DateRangeOptions) {
150+
constructor(docsUrl: string, dateRange: DateRangeOptions, selectedDomain?: string) {
149151
this.docsUrl = docsUrl;
150152
this.dateRange = dateRange;
153+
this.selectedDomain = selectedDomain;
151154
}
152155

153156
async getSupabaseCache(): Promise<CachedAnalytics | null> {
@@ -161,7 +164,8 @@ class AnalyticsQueryHandler {
161164
return null;
162165
}
163166

164-
const docsSiteKey = getDocsSiteKey(this.docsUrl);
167+
// Use selectedDomain if provided, otherwise extract from docsUrl
168+
const docsSiteKey = this.selectedDomain || getDocsSiteKey(this.docsUrl);
165169
const startTime = Date.now();
166170
this.supabaseCache = await getCachedAnalytics({
167171
docsSite: docsSiteKey,
@@ -180,11 +184,11 @@ class AnalyticsQueryHandler {
180184

181185
const handlerCache = new Map<string, AnalyticsQueryHandler>();
182186

183-
function getHandler(docsUrl: string, dateRange: DateRangeOptions): AnalyticsQueryHandler {
184-
const key = `${docsUrl}:${JSON.stringify(dateRange)}`;
187+
function getHandler(docsUrl: string, dateRange: DateRangeOptions, selectedDomain?: string): AnalyticsQueryHandler {
188+
const key = `${docsUrl}:${JSON.stringify(dateRange)}:${selectedDomain || ""}`;
185189
let handler = handlerCache.get(key);
186190
if (!handler) {
187-
handler = new AnalyticsQueryHandler(docsUrl, dateRange);
191+
handler = new AnalyticsQueryHandler(docsUrl, dateRange, selectedDomain);
188192
handlerCache.set(key, handler);
189193
setTimeout(() => handlerCache.delete(key), 60000);
190194
}
@@ -211,9 +215,10 @@ async function getLiveAnalytics(docsUrl: string) {
211215
export async function getWebAnalytics(request: GetWebAnalyticsRequest): Promise<GetWebAnalyticsResponse> {
212216
const validated = GetWebAnalyticsSchema.parse(request);
213217
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
214-
const baseDomain = getBaseDomain(validated.docsUrl);
218+
// Use selectedDomain if provided, otherwise extract from docsUrl
219+
const baseDomain = validated.selectedDomain || getBaseDomain(validated.docsUrl);
215220

216-
const handler = getHandler(validated.docsUrl, dateRange);
221+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
217222
const supabaseCache = await handler.getSupabaseCache();
218223

219224
if (supabaseCache) {
@@ -256,9 +261,10 @@ export async function getWebAnalytics(request: GetWebAnalyticsRequest): Promise<
256261
export async function getAllAnalytics(request: GetWebAnalyticsRequest): Promise<AllAnalyticsResponse> {
257262
const validated = GetWebAnalyticsSchema.parse(request);
258263
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
259-
const baseDomain = getBaseDomain(validated.docsUrl);
264+
// Use selectedDomain if provided, otherwise extract from docsUrl
265+
const baseDomain = validated.selectedDomain || getBaseDomain(validated.docsUrl);
260266

261-
const handler = getHandler(validated.docsUrl, dateRange);
267+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
262268
const supabaseCache = await handler.getSupabaseCache();
263269

264270
if (supabaseCache && !validated.groupBy) {
@@ -395,7 +401,7 @@ export async function getPageViewsByDay(
395401
const validated = GetWebAnalyticsSchema.parse(request);
396402
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
397403

398-
const handler = getHandler(validated.docsUrl, dateRange);
404+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
399405
const supabaseCache = await handler.getSupabaseCache();
400406

401407
if (supabaseCache && !validated.groupBy) {
@@ -425,7 +431,7 @@ export async function getVisitorsByDay(
425431
const validated = GetWebAnalyticsSchema.parse(request);
426432
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
427433

428-
const handler = getHandler(validated.docsUrl, dateRange);
434+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
429435
const supabaseCache = await handler.getSupabaseCache();
430436

431437
if (supabaseCache && !validated.groupBy) {
@@ -458,7 +464,7 @@ export async function getTopPages(
458464
const orderBy = validated.orderBy || "views";
459465
const order = validated.order || "desc";
460466

461-
const handler = getHandler(validated.docsUrl, dateRange);
467+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
462468
const supabaseCache = await handler.getSupabaseCache();
463469

464470
if (supabaseCache) {
@@ -494,7 +500,7 @@ export async function getTopCountries(request: TableRequest): Promise<{
494500
const orderBy = validated.orderBy || "visitors";
495501
const order = validated.order || "desc";
496502

497-
const handler = getHandler(validated.docsUrl, dateRange);
503+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
498504
const supabaseCache = await handler.getSupabaseCache();
499505

500506
if (supabaseCache) {
@@ -533,7 +539,7 @@ export async function getLLMFileViews(request: LLMFileViewsRequest): Promise<{
533539
const validated = LLMFileViewsRequestSchema.parse(request);
534540
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
535541

536-
const handler = getHandler(validated.docsUrl, dateRange);
542+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
537543
const supabaseCache = await handler.getSupabaseCache();
538544

539545
if (supabaseCache) {
@@ -562,7 +568,7 @@ export async function getChannels(request: TableRequest): Promise<{
562568
const validated = TableRequestSchema.parse(request);
563569
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
564570

565-
const handler = getHandler(validated.docsUrl, dateRange);
571+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
566572
const supabaseCache = await handler.getSupabaseCache();
567573

568574
if (supabaseCache) {
@@ -591,7 +597,7 @@ export async function getDeviceTypes(request: TableRequest): Promise<{
591597
const validated = TableRequestSchema.parse(request);
592598
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
593599

594-
const handler = getHandler(validated.docsUrl, dateRange);
600+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
595601
const supabaseCache = await handler.getSupabaseCache();
596602

597603
if (supabaseCache) {
@@ -620,7 +626,7 @@ export async function getReferringDomains(request: TableRequest): Promise<{
620626
const validated = TableRequestSchema.parse(request);
621627
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
622628

623-
const handler = getHandler(validated.docsUrl, dateRange);
629+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
624630
const supabaseCache = await handler.getSupabaseCache();
625631

626632
if (supabaseCache) {
@@ -672,7 +678,7 @@ export async function getAPIExplorerRequests(request: TableRequest): Promise<{
672678
const validated = TableRequestSchema.parse(request);
673679
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
674680

675-
const handler = getHandler(validated.docsUrl, dateRange);
681+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
676682
const supabaseCache = await handler.getSupabaseCache();
677683

678684
if (supabaseCache) {
@@ -705,7 +711,7 @@ export async function getLLMBotTrafficByProvider(request: TableRequest): Promise
705711
const validated = TableRequestSchema.parse(request);
706712
const dateRange = validated.dateRange || DEFAULT_DATE_RANGE;
707713

708-
const handler = getHandler(validated.docsUrl, dateRange);
714+
const handler = getHandler(validated.docsUrl, dateRange, validated.selectedDomain);
709715
const supabaseCache = await handler.getSupabaseCache();
710716

711717
if (supabaseCache) {

packages/fern-dashboard/src/app/services/analyticsCron/getAllProductionDomains.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export async function getAllProductionDomains(): Promise<ProductionDomain[]> {
7979
// If docs KV is available with CDN_URI, try that first (faster)
8080
if (docsKv && cdnUri) {
8181
const kvDomains = await getAllProductionDomainsFromKV(docsKv, cdnUri);
82+
console.log("[getAllProductionDomains] KV returned!!!", kvDomains.length, "domains");
8283
if (kvDomains.length > 0) {
8384
return kvDomains;
8485
}
@@ -106,7 +107,7 @@ async function getAllProductionDomainsFromKV(kv: any, cdnUri: string): Promise<P
106107
/**
107108
* Get production domains from FDR (Fern Definition Registry)
108109
*/
109-
async function getAllProductionDomainsFromFDR(): Promise<ProductionDomain[]> {
110+
export async function getAllProductionDomainsFromFDR(): Promise<ProductionDomain[]> {
110111
const fdrServerUrl = process.env.FDR_SERVER_URL ?? process.env.NEXT_PUBLIC_FDR_ORIGIN;
111112
const fernToken = process.env.FERN_TOKEN;
112113

packages/fern-dashboard/src/components/web-analytics/AnalyticsDataContext.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ interface AnalyticsDataProviderProps {
2121
docsUrl: string;
2222
dateRange: GetWebAnalyticsRequest["dateRange"];
2323
groupBy?: number | undefined;
24+
selectedDomain?: string | null;
2425
}
2526

26-
export function AnalyticsDataProvider({ children, docsUrl, dateRange, groupBy }: AnalyticsDataProviderProps) {
27+
export function AnalyticsDataProvider({
28+
children,
29+
docsUrl,
30+
dateRange,
31+
groupBy,
32+
selectedDomain
33+
}: AnalyticsDataProviderProps) {
2734
const { data, isLoading, error } = useQuery({
28-
queryKey: ["all-analytics", docsUrl, dateRange, groupBy],
29-
queryFn: () => getAllAnalytics({ docsUrl, dateRange, groupBy }),
35+
queryKey: ["all-analytics", docsUrl, dateRange, groupBy, selectedDomain],
36+
queryFn: () => getAllAnalytics({ docsUrl, dateRange, groupBy, selectedDomain: selectedDomain || undefined }),
3037
staleTime: ANALYTICS_STALE_TIME
3138
});
3239

0 commit comments

Comments
 (0)