Skip to content

Commit

Permalink
analytics gtm ssr (#1921)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Dec 20, 2024
1 parent 6506692 commit d951b9a
Show file tree
Hide file tree
Showing 21 changed files with 299 additions and 73 deletions.
4 changes: 2 additions & 2 deletions packages/fern-docs/bundle/src/pages/404.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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 <Error statusCode={404} />;
Expand Down
1 change: 0 additions & 1 deletion packages/fern-docs/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 3 additions & 7 deletions packages/fern-docs/ui/src/analytics/CustomerAnalytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -57,7 +53,7 @@ export const CustomerAnalytics = memo(
<IntercomScript config={config.intercom} />
<FullstoryScript config={config.fullstory} />

{/* renders Google Analytics 4 or Google Tag Manager using @next/third-parties */}
{/* renders Google Analytics 4 or Google Tag Manager */}
{ga4 != null && <GoogleAnalytics gaId={ga4.measurementId} />}
{gtm != null && <GoogleTagManager gtmId={gtm.tagId} />}
</>
Expand Down
13 changes: 12 additions & 1 deletion packages/fern-docs/ui/src/analytics/PosthogContainer.tsx
Original file line number Diff line number Diff line change
@@ -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 <></>;
}
1 change: 1 addition & 0 deletions packages/fern-docs/ui/src/analytics/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const TRACK_EVENT_NAME = "fern-docs-track-analytics";
70 changes: 70 additions & 0 deletions packages/fern-docs/ui/src/analytics/ga.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<script
key="ga"
id="_fern-ga-init"
dangerouslySetInnerHTML={{
__html: `
window['${dataLayerName}'] = window['${dataLayerName}'] || [];
function gtag(){window['${dataLayerName}'].push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ""});`,
}}
nonce={nonce}
/>
</Head>
<Script
id="_fern-ga"
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
nonce={nonce}
/>
</>
);
}

function sendGAEvent(dataLayer: string, ...args: unknown[]): void {
if (window[dataLayer]) {
window[dataLayer].push(...args);
} else {
console.warn(`GA dataLayer ${dataLayer} does not exist`);
}
}
73 changes: 73 additions & 0 deletions packages/fern-docs/ui/src/analytics/gtm.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Head>
<script
key="ga"
id="_fern-gtm"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='${dataLayerName}'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','${dataLayerName}','${gtmId}');
`,
}}
nonce={nonce}
/>
</Head>
<noscript nonce={nonce} id="_fern-gtm-noscript">
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
height="0"
width="0"
style={{ display: "none", visibility: "hidden" }}
nonce={nonce}
/>
</noscript>
</>
);
}

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);
};
1 change: 1 addition & 0 deletions packages/fern-docs/ui/src/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { track, trackInternal } from "./track";
13 changes: 11 additions & 2 deletions packages/fern-docs/ui/src/analytics/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,20 @@ export function resetPosthog(): void {
});
}

export function capturePosthogEvent(
export function capturePosthogEventInternal(
eventName: string,
properties?: Record<string, unknown>
): void {
void safeAccessPosthog((posthog) => {
posthog.capture(eventName, properties);
});
}

export function capturePosthogEventCustomer(
eventName: string,
properties?: Record<string, unknown>
): void {
void safeAccessPosthog((posthog) => {
ifCustomer(posthog, (posthog) =>
posthog.customer.capture(eventName, properties)
);
Expand All @@ -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" &&
Expand Down
41 changes: 41 additions & 0 deletions packages/fern-docs/ui/src/analytics/track.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>
): 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<string, unknown>
): void {
if (typeof window === "undefined") {
return;
}

window.dispatchEvent(
new CustomEvent(TRACK_EVENT_NAME, {
detail: { event, properties, internal: true },
})
);
}
57 changes: 57 additions & 0 deletions packages/fern-docs/ui/src/analytics/use-track.ts
Original file line number Diff line number Diff line change
@@ -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<typeof TrackEventSchema>;

/**
* 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]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit d951b9a

Please sign in to comment.