diff --git a/packages/fern-docs/bundle/src/pages/404.tsx b/packages/fern-docs/bundle/src/pages/404.tsx
index 336c1f2f5b..4c21ddd497 100644
--- a/packages/fern-docs/bundle/src/pages/404.tsx
+++ b/packages/fern-docs/bundle/src/pages/404.tsx
@@ -1,4 +1,4 @@
-import { capturePosthogEvent } from "@fern-docs/ui";
+import { track } from "@fern-docs/ui";
import Error from "next/error";
import { ReactElement, useEffect } from "react";
@@ -12,7 +12,7 @@ import { ReactElement, useEffect } from "react";
// If you use initial props, the middleware's response will probably cause a client-side error to be thrown.
export default function Page(): ReactElement {
useEffect(() => {
- capturePosthogEvent("not_found");
+ track("not_found");
});
return ;
diff --git a/packages/fern-docs/ui/package.json b/packages/fern-docs/ui/package.json
index 7f7a7441b4..990103d372 100644
--- a/packages/fern-docs/ui/package.json
+++ b/packages/fern-docs/ui/package.json
@@ -59,7 +59,6 @@
"@fern-ui/loadable": "workspace:*",
"@fern-ui/react-commons": "workspace:*",
"@inkeep/widgets": "^0.2.288",
- "@next/third-parties": "14.2.9",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-collapsible": "^1.1.1",
diff --git a/packages/fern-docs/ui/src/analytics/CustomerAnalytics.tsx b/packages/fern-docs/ui/src/analytics/CustomerAnalytics.tsx
index 2280826221..e11b88ea11 100644
--- a/packages/fern-docs/ui/src/analytics/CustomerAnalytics.tsx
+++ b/packages/fern-docs/ui/src/analytics/CustomerAnalytics.tsx
@@ -20,12 +20,8 @@ const IntercomScript = dynamic(() =>
const FullstoryScript = dynamic(() =>
import("./FullstoryScript").then((mod) => mod.FullstoryScript)
);
-const GoogleAnalytics = dynamic(() =>
- import("@next/third-parties/google").then((mod) => mod.GoogleAnalytics)
-);
-const GoogleTagManager = dynamic(() =>
- import("@next/third-parties/google").then((mod) => mod.GoogleTagManager)
-);
+const GoogleAnalytics = dynamic(() => import("./ga"), { ssr: true });
+const GoogleTagManager = dynamic(() => import("./gtm"), { ssr: true });
const ANALYTICS_ATOM = selectAtom(
DOCS_ATOM,
@@ -57,7 +53,7 @@ export const CustomerAnalytics = memo(
- {/* renders Google Analytics 4 or Google Tag Manager using @next/third-parties */}
+ {/* renders Google Analytics 4 or Google Tag Manager */}
{ga4 != null && }
{gtm != null && }
>
diff --git a/packages/fern-docs/ui/src/analytics/PosthogContainer.tsx b/packages/fern-docs/ui/src/analytics/PosthogContainer.tsx
index 0655f4a3be..8a0ea9903b 100644
--- a/packages/fern-docs/ui/src/analytics/PosthogContainer.tsx
+++ b/packages/fern-docs/ui/src/analytics/PosthogContainer.tsx
@@ -1,10 +1,21 @@
import { DocsV1Read } from "@fern-api/fdr-sdk";
import { ReactElement } from "react";
-import { useInitializePosthog } from "./posthog";
+import {
+ capturePosthogEventCustomer,
+ capturePosthogEventInternal,
+ useInitializePosthog,
+} from "./posthog";
+import { useSafeListenTrackEvents } from "./use-track";
export function Posthog(props: {
customerConfig?: DocsV1Read.PostHogConfig;
}): ReactElement {
useInitializePosthog(props.customerConfig);
+ useSafeListenTrackEvents(({ event, properties }) => {
+ capturePosthogEventCustomer(event, properties);
+ });
+ useSafeListenTrackEvents(({ event, properties }) => {
+ capturePosthogEventInternal(event, properties);
+ }, true);
return <>>;
}
diff --git a/packages/fern-docs/ui/src/analytics/constants.ts b/packages/fern-docs/ui/src/analytics/constants.ts
new file mode 100644
index 0000000000..b1a18bcccc
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/constants.ts
@@ -0,0 +1 @@
+export const TRACK_EVENT_NAME = "fern-docs-track-analytics";
diff --git a/packages/fern-docs/ui/src/analytics/ga.tsx b/packages/fern-docs/ui/src/analytics/ga.tsx
new file mode 100644
index 0000000000..679ff5e591
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/ga.tsx
@@ -0,0 +1,70 @@
+import Head from "next/head";
+import Script from "next/script";
+import { ReactNode, useEffect } from "react";
+import { useSafeListenTrackEvents } from "./use-track";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
+ interface Window {
+ [key: string]: any;
+ }
+}
+
+type GAParams = {
+ gaId: string;
+ dataLayerName?: string;
+ debugMode?: boolean;
+ nonce?: string;
+};
+
+export default function GoogleAnalytics(props: GAParams): ReactNode {
+ const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = props;
+ useEffect(() => {
+ // performance.mark is being used as a feature use signal. While it is traditionally used for performance
+ // benchmarking it is low overhead and thus considered safe to use in production and it is a widely available
+ // existing API.
+ // The performance measurement will be handled by Chrome Aurora
+
+ performance.mark("mark_feature_usage", {
+ detail: {
+ feature: "fern-analytics-ga",
+ },
+ });
+ }, []);
+
+ useSafeListenTrackEvents(({ event, properties }) => {
+ sendGAEvent(dataLayerName, { event, properties });
+ });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+function sendGAEvent(dataLayer: string, ...args: unknown[]): void {
+ if (window[dataLayer]) {
+ window[dataLayer].push(...args);
+ } else {
+ console.warn(`GA dataLayer ${dataLayer} does not exist`);
+ }
+}
diff --git a/packages/fern-docs/ui/src/analytics/gtm.tsx b/packages/fern-docs/ui/src/analytics/gtm.tsx
new file mode 100644
index 0000000000..c47fe4bb4b
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/gtm.tsx
@@ -0,0 +1,73 @@
+import Head from "next/head";
+import { ReactNode, useEffect } from "react";
+import { useSafeListenTrackEvents } from "./use-track";
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
+ interface Window {
+ [key: string]: any;
+ }
+}
+
+type GTMParams = {
+ gtmId: string;
+ dataLayerName?: string;
+ nonce?: string;
+};
+
+export default function GoogleTagManager(props: GTMParams): ReactNode {
+ const { gtmId, dataLayerName = "dataLayer", nonce } = props;
+
+ useEffect(() => {
+ // performance.mark is being used as a feature use signal. While it is traditionally used for performance
+ // benchmarking it is low overhead and thus considered safe to use in production and it is a widely available
+ // existing API.
+ // The performance measurement will be handled by Chrome Aurora
+
+ performance.mark("mark_feature_usage", {
+ detail: {
+ feature: "fern-analytics-gtm",
+ },
+ });
+ }, []);
+
+ useSafeListenTrackEvents(({ event, properties }) => {
+ sendGTMEvent(dataLayerName, { event, properties });
+ });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+
+export const sendGTMEvent = (dataLayer: string, data: unknown): void => {
+ // define dataLayer so we can still queue up events before GTM init
+ window[dataLayer] = window[dataLayer] || [];
+ window[dataLayer].push(data);
+};
diff --git a/packages/fern-docs/ui/src/analytics/index.ts b/packages/fern-docs/ui/src/analytics/index.ts
new file mode 100644
index 0000000000..f77acad6fb
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/index.ts
@@ -0,0 +1 @@
+export { track, trackInternal } from "./track";
diff --git a/packages/fern-docs/ui/src/analytics/posthog.ts b/packages/fern-docs/ui/src/analytics/posthog.ts
index cfe5dcded7..bf117e0da3 100644
--- a/packages/fern-docs/ui/src/analytics/posthog.ts
+++ b/packages/fern-docs/ui/src/analytics/posthog.ts
@@ -135,12 +135,20 @@ export function resetPosthog(): void {
});
}
-export function capturePosthogEvent(
+export function capturePosthogEventInternal(
eventName: string,
properties?: Record
): void {
void safeAccessPosthog((posthog) => {
posthog.capture(eventName, properties);
+ });
+}
+
+export function capturePosthogEventCustomer(
+ eventName: string,
+ properties?: Record
+): void {
+ void safeAccessPosthog((posthog) => {
ifCustomer(posthog, (posthog) =>
posthog.customer.capture(eventName, properties)
);
@@ -149,7 +157,8 @@ export function capturePosthogEvent(
const trackPageView = (url: string) => {
safeCall(() => {
- capturePosthogEvent("$pageview");
+ capturePosthogEventCustomer("$pageview");
+ capturePosthogEventInternal("$pageview");
typeof window !== "undefined" &&
window?.analytics &&
typeof window.analytics.page === "function" &&
diff --git a/packages/fern-docs/ui/src/analytics/track.ts b/packages/fern-docs/ui/src/analytics/track.ts
new file mode 100644
index 0000000000..048a1f5c74
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/track.ts
@@ -0,0 +1,41 @@
+import { TRACK_EVENT_NAME } from "./constants";
+
+/**
+ * Track an event.
+ *
+ * @param event - The event name.
+ * @param properties - The event properties.
+ */
+export function track(
+ event: string,
+ properties?: Record
+): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent(TRACK_EVENT_NAME, { detail: { event, properties } })
+ );
+}
+
+/**
+ * Track an event that is only for internal use.
+ *
+ * @param event - The event name.
+ * @param properties - The event properties.
+ */
+export function trackInternal(
+ event: string,
+ properties?: Record
+): void {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ window.dispatchEvent(
+ new CustomEvent(TRACK_EVENT_NAME, {
+ detail: { event, properties, internal: true },
+ })
+ );
+}
diff --git a/packages/fern-docs/ui/src/analytics/use-track.ts b/packages/fern-docs/ui/src/analytics/use-track.ts
new file mode 100644
index 0000000000..365316d2f3
--- /dev/null
+++ b/packages/fern-docs/ui/src/analytics/use-track.ts
@@ -0,0 +1,57 @@
+import React from "react";
+import { z } from "zod";
+import { TRACK_EVENT_NAME } from "./constants";
+
+const TrackEventSchema = z.object({
+ event: z.string(),
+ properties: z
+ .record(
+ z.union([
+ z.string(),
+ z.number(),
+ z.boolean(),
+ z.string().array().readonly(),
+ z.number().array().readonly(),
+ z.boolean().array().readonly(),
+ z.undefined(),
+ ])
+ )
+ .optional(),
+ internal: z.boolean().optional(),
+});
+
+type TrackEvent = z.infer;
+
+/**
+ * Listen for track events, and emit them to analytics integrations.
+ *
+ * @param cb - The callback to emit the event to.
+ * @param allowInternal - Whether to allow internal events to be emitted. Defaults to false. Only use this for Fern's internal posthog instance.
+ */
+export function useSafeListenTrackEvents(
+ cb: (detail: TrackEvent) => void,
+ allowInternal = false
+): void {
+ const ref = React.useRef<(detail: TrackEvent) => void>(cb);
+
+ React.useEffect(() => {
+ ref.current = cb;
+ });
+
+ React.useEffect(() => {
+ const handler = (event: Event) => {
+ try {
+ if (event instanceof CustomEvent) {
+ const detail = TrackEventSchema.safeParse(event.detail);
+ if (detail.success && (allowInternal || !detail.data.internal)) {
+ ref.current(detail.data);
+ }
+ }
+ } catch (error) {
+ console.warn("Error emitting track event", error, event);
+ }
+ };
+ window.addEventListener(TRACK_EVENT_NAME, handler);
+ return () => window.removeEventListener(TRACK_EVENT_NAME, handler);
+ }, [allowInternal]);
+}
diff --git a/packages/fern-docs/ui/src/api-reference/endpoints/EndpointParameter.tsx b/packages/fern-docs/ui/src/api-reference/endpoints/EndpointParameter.tsx
index dcb6c8c69a..ecbf76e02c 100644
--- a/packages/fern-docs/ui/src/api-reference/endpoints/EndpointParameter.tsx
+++ b/packages/fern-docs/ui/src/api-reference/endpoints/EndpointParameter.tsx
@@ -14,7 +14,7 @@ import {
useRef,
useState,
} from "react";
-import { capturePosthogEvent } from "../../analytics/posthog";
+import { trackInternal } from "../../analytics";
import { useIsApiReferencePaginated, useRouteListener } from "../../atoms";
import { FernAnchor } from "../../components/FernAnchor";
import { useHref } from "../../hooks/useHref";
@@ -124,7 +124,7 @@ export const EndpointParameterContent: FC<
useEffect(() => {
if (descriptions.length > 0) {
- capturePosthogEvent("api_reference_multiple_descriptions", {
+ trackInternal("api_reference_multiple_descriptions", {
name,
slug,
anchorIdParts,
diff --git a/packages/fern-docs/ui/src/api-reference/types/discriminated-union/DiscriminatedUnionVariant.tsx b/packages/fern-docs/ui/src/api-reference/types/discriminated-union/DiscriminatedUnionVariant.tsx
index 01e9f521df..d2eddd2bb3 100644
--- a/packages/fern-docs/ui/src/api-reference/types/discriminated-union/DiscriminatedUnionVariant.tsx
+++ b/packages/fern-docs/ui/src/api-reference/types/discriminated-union/DiscriminatedUnionVariant.tsx
@@ -6,7 +6,7 @@ import { AvailabilityBadge } from "@fern-docs/components/badges";
import cn from "clsx";
import { compact } from "es-toolkit/array";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { capturePosthogEvent } from "../../../analytics/posthog";
+import { trackInternal } from "../../../analytics";
import { useIsApiReferencePaginated, useRouteListener } from "../../../atoms";
import { FernAnchor } from "../../../components/FernAnchor";
import { useHref } from "../../../hooks/useHref";
@@ -96,7 +96,7 @@ export const DiscriminatedUnionVariant: React.FC<
useEffect(() => {
if (descriptions.length > 0) {
- capturePosthogEvent("api_reference_multiple_descriptions", {
+ trackInternal("api_reference_multiple_descriptions", {
slug,
anchorIdParts,
discriminant,
diff --git a/packages/fern-docs/ui/src/api-reference/types/object/ObjectProperty.tsx b/packages/fern-docs/ui/src/api-reference/types/object/ObjectProperty.tsx
index 30a1616902..9d1d4286af 100644
--- a/packages/fern-docs/ui/src/api-reference/types/object/ObjectProperty.tsx
+++ b/packages/fern-docs/ui/src/api-reference/types/object/ObjectProperty.tsx
@@ -12,7 +12,7 @@ import {
useRef,
useState,
} from "react";
-import { capturePosthogEvent } from "../../../analytics/posthog";
+import { trackInternal } from "../../../analytics";
import { useIsApiReferencePaginated, useRouteListener } from "../../../atoms";
import { FernAnchor } from "../../../components/FernAnchor";
import { FernErrorBoundary } from "../../../components/FernErrorBoundary";
@@ -138,7 +138,7 @@ const UnmemoizedObjectPropertyInternal = forwardRef<
useEffect(() => {
if (descriptions.length > 0) {
- capturePosthogEvent("api_reference_multiple_descriptions", {
+ trackInternal("api_reference_multiple_descriptions", {
name: property.key,
slug,
anchorIdParts,
diff --git a/packages/fern-docs/ui/src/components/JavascriptProvider.tsx b/packages/fern-docs/ui/src/components/JavascriptProvider.tsx
index 2c05bba2da..7d0e037849 100644
--- a/packages/fern-docs/ui/src/components/JavascriptProvider.tsx
+++ b/packages/fern-docs/ui/src/components/JavascriptProvider.tsx
@@ -1,7 +1,6 @@
import { atom, useAtomValue } from "jotai";
import Script from "next/script";
import { memo } from "react";
-import { CustomerAnalytics } from "../analytics/CustomerAnalytics";
import { DOCS_ATOM, FILES_ATOM } from "../atoms";
const JS_ATOM = atom((get) => get(DOCS_ATOM).js);
@@ -29,7 +28,6 @@ export const JavascriptProvider = memo(() => {
{js?.remote?.map((remote) => (
))}
-
>
);
});
diff --git a/packages/fern-docs/ui/src/docs/NextApp.tsx b/packages/fern-docs/ui/src/docs/NextApp.tsx
index ca3dbe7d24..ec80980b15 100644
--- a/packages/fern-docs/ui/src/docs/NextApp.tsx
+++ b/packages/fern-docs/ui/src/docs/NextApp.tsx
@@ -12,6 +12,7 @@ import "../css/globals.scss";
import { NextNProgress } from "../header/NProgress";
import { useInterceptNextDataHref } from "../hooks/useInterceptNextDataHref";
import { ThemeScript } from "../themes/ThemeScript";
+import { CustomerAnalytics } from "../analytics/CustomerAnalytics";
export function NextApp({
Component,
@@ -33,6 +34,7 @@ export function NextApp({
options={{ showSpinner: false, speed: 400 }}
showOnShallow={false}
/>
+
= ({ className }) => {
setIsHelpful(true);
setShowFeedbackInput(true);
textareaRef.current?.focus();
- capturePosthogEvent("feedback_voted", {
+ track("feedback_voted", {
satisfied: true,
});
};
@@ -47,7 +45,7 @@ export const Feedback: FC = ({ className }) => {
setIsHelpful(false);
setShowFeedbackInput(true);
textareaRef.current?.focus();
- capturePosthogEvent("feedback_voted", {
+ track("feedback_voted", {
satisfied: false,
});
};
@@ -65,7 +63,7 @@ export const Feedback: FC = ({ className }) => {
showEmailInput: boolean | "indeterminate";
}) => {
registerPosthogProperties({ email });
- capturePosthogEvent("feedback_submitted", {
+ track("feedback_submitted", {
satisfied: isHelpful ? true : false,
feedback: feedbackId,
message: feedbackMessage,
@@ -106,9 +104,7 @@ export const Feedback: FC = ({ className }) => {
}
variant="outlined"
diff --git a/packages/fern-docs/ui/src/feedback/FeedbackPopover.tsx b/packages/fern-docs/ui/src/feedback/FeedbackPopover.tsx
index ab32594fcf..cc78a2f3b5 100644
--- a/packages/fern-docs/ui/src/feedback/FeedbackPopover.tsx
+++ b/packages/fern-docs/ui/src/feedback/FeedbackPopover.tsx
@@ -4,7 +4,7 @@ import { motion } from "framer-motion";
import { Check, ThumbsDown, ThumbsUp } from "iconoir-react";
import { forwardRef, useCallback, useMemo, useState } from "react";
import * as Selection from "selection-popover";
-import { capturePosthogEvent } from "../analytics/posthog";
+import { track } from "../analytics";
import { useSelection } from "../hooks/useSelection";
import { FeedbackForm } from "./FeedbackForm";
@@ -85,7 +85,7 @@ export const FeedbackPopover = forwardRef<
const handleThumbsUp = useCallback(() => {
setIsHelpful(true);
- capturePosthogEvent("feedback_voted", {
+ track("feedback_voted", {
satisfied: true,
selectedText: selection?.toString().trim(),
});
@@ -93,7 +93,7 @@ export const FeedbackPopover = forwardRef<
const handleThumbsDown = useCallback(() => {
setIsHelpful(false);
- capturePosthogEvent("feedback_voted", {
+ track("feedback_voted", {
satisfied: false,
selectedText: selection?.toString().trim(),
});
@@ -108,7 +108,7 @@ export const FeedbackPopover = forwardRef<
email: string;
showEmailInput: boolean | "indeterminate";
}) => {
- capturePosthogEvent("feedback_submitted", {
+ track("feedback_submitted", {
satisfied: isHelpful,
feedback: feedbackId,
selectedText: selection?.toString().trim(),
@@ -154,13 +154,13 @@ export const FeedbackPopover = forwardRef<
icon={
}
variant="minimal"
- intent={isHelpful === false ? "danger" : "none"}
- active={isHelpful === false}
+ intent={!isHelpful ? "danger" : "none"}
+ active={!isHelpful}
onClick={handleThumbsDown}
className={clsx({ "w-full": isHelpful !== undefined })}
transition={{ type: "spring", duration: 0.3, bounce: 0 }}
@@ -201,11 +201,7 @@ export const FeedbackPopover = forwardRef<
{isHelpful !== undefined &&
(isFeedbackSubmitted ? (
@@ -233,11 +229,7 @@ export const FeedbackPopover = forwardRef<
) : (
= 8'}
@@ -15080,9 +15071,6 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
- third-party-capital@1.0.20:
- resolution: {integrity: sha512-oB7yIimd8SuGptespDAZnNkzIz+NWaJCu2RMsbs4Wmp9zSDUM8Nhi3s2OOcqYuv3mN4hitXc8DVx+LyUmbUDiA==}
-
three-mesh-bvh@0.7.8:
resolution: {integrity: sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==}
deprecated: Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.
@@ -19230,12 +19218,6 @@ snapshots:
'@next/swc-win32-x64-msvc@15.1.2':
optional: true
- '@next/third-parties@14.2.9(@fern-api/next@14.2.9-fork.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.0))(react@18.3.1)':
- dependencies:
- next: '@fern-api/next@14.2.9-fork.2(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.0)'
- react: 18.3.1
- third-party-capital: 1.0.20
-
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -32453,8 +32435,6 @@ snapshots:
dependencies:
any-promise: 1.3.0
- third-party-capital@1.0.20: {}
-
three-mesh-bvh@0.7.8(three@0.171.0):
dependencies:
three: 0.171.0