Skip to content

Commit 8d09ab3

Browse files
Merge pull request #1650 from codatio/PEP-379
Add Amplitude Tracking
2 parents 2799e98 + 7e152ee commit 8d09ab3

File tree

8 files changed

+752
-222
lines changed

8 files changed

+752
-222
lines changed

docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const config = {
4848
ZENDESK_KEY: process.env.ZENDESK_KEY,
4949
FEATURE_DEV_FLAG: process.env.FEATURE_DEV_FLAG,
5050
FEATURE_NEW_PRODUCTS_FLAG: process.env.FEATURE_NEW_PRODUCTS_FLAG,
51+
AMPLITUDE_API_KEY: process.env.AMPLITUDE_API_KEY,
5152
},
5253

5354
presets: [

package-lock.json

Lines changed: 150 additions & 221 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"format:mdx:check": "prettier --check \"**/*.{md,mdx}\""
2222
},
2323
"dependencies": {
24+
"@amplitude/analytics-browser": "^2.23.3",
2425
"@codat/sdk-link-types": "^1.7.0",
2526
"@docusaurus/core": "^3.4.0",
2627
"@docusaurus/plugin-client-redirects": "^3.4.0",

src/theme/MDXComponents/A.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
import React from "react";
22
import Link from "@docusaurus/Link";
3+
import { trackLinkClick } from "../../utils/amplitude";
4+
35
export default function MDXA(props) {
4-
return <Link {...props} />;
6+
const handleClick = () => {
7+
if (props.href) {
8+
const isExternal =
9+
props.href.startsWith("http") || props.href.startsWith("//");
10+
const isDownload =
11+
props.href.includes("download") ||
12+
props.href.endsWith(".pdf") ||
13+
props.href.endsWith(".zip") ||
14+
props.href.endsWith(".doc") ||
15+
props.href.endsWith(".docx") ||
16+
props.href.endsWith(".xlsx");
17+
18+
let linkType = "internal";
19+
if (isExternal) linkType = "external";
20+
if (isDownload) linkType = "download";
21+
22+
trackLinkClick(props.href, props.children || props.title, linkType, {
23+
component: "mdx_link",
24+
});
25+
}
26+
};
27+
28+
return <Link {...props} onClick={handleClick} />;
529
}

src/theme/NavbarItem/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import NavbarSeparator from "@theme/NavbarItem/NavbarSeparator";
55
import NavbarCta from "@theme/NavbarItem/NavbarCta";
66

77
import { track } from "@vercel/analytics";
8+
import { trackEvent } from "../../utils/amplitude";
89

910
const CustomNavbarItemComponents = {
1011
iconLink: () => NavbarIconLink,
@@ -18,6 +19,16 @@ export default function NavbarItem(props) {
1819
const onTrack = () => {
1920
if (props.label === "Sign in") {
2021
track("Sign in");
22+
trackEvent("Navigation Click", {
23+
element: "Sign in",
24+
type: "navbar_cta",
25+
});
26+
} else if (props.label) {
27+
trackEvent("Navigation Click", {
28+
element: props.label,
29+
type: "navbar_item",
30+
href: props.href || props.to,
31+
});
2132
}
2233
};
2334

src/theme/Root.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { useEffect } from "react";
2+
import { useLocation } from "@docusaurus/router";
3+
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
4+
import {
5+
trackPageView,
6+
initAmplitude,
7+
trackDocSection,
8+
} from "../utils/amplitude";
9+
import "../utils/clientModules";
10+
11+
export default function Root({ children }) {
12+
const location = useLocation();
13+
const { siteConfig } = useDocusaurusContext();
14+
15+
// Initialize Amplitude on mount
16+
useEffect(() => {
17+
// Get API key from site config or environment variables
18+
const apiKey =
19+
siteConfig.customFields?.AMPLITUDE_API_KEY ||
20+
process.env.REACT_APP_AMPLITUDE_API_KEY;
21+
22+
if (apiKey) {
23+
initAmplitude(apiKey);
24+
} else {
25+
console.warn(
26+
"Amplitude API key not found. Set AMPLITUDE_API_KEY in customFields or REACT_APP_AMPLITUDE_API_KEY environment variable.",
27+
);
28+
}
29+
}, [siteConfig]);
30+
31+
// Track page views on route changes
32+
useEffect(() => {
33+
// Small delay to ensure page title is updated
34+
const timer = setTimeout(() => {
35+
trackPageView();
36+
37+
// Track specific documentation sections
38+
const path = location.pathname;
39+
console.log("path", path);
40+
if (path.includes("/auth-flow")) {
41+
trackDocSection("Authentication Flow");
42+
} else if (path.includes("/bank-feeds")) {
43+
trackDocSection("Bank Feeds");
44+
} else if (path.includes("/commerce")) {
45+
trackDocSection("Commerce");
46+
} else if (path.includes("/lending")) {
47+
trackDocSection("Lending");
48+
} else if (path.includes("/payables")) {
49+
trackDocSection("Payables");
50+
} else if (path.includes("/payroll")) {
51+
trackDocSection("Payroll");
52+
} else if (path.includes("/expenses")) {
53+
trackDocSection("Expenses");
54+
} else if (path.includes("/using-the-api")) {
55+
trackDocSection("Using the API");
56+
} else if (path.includes("/get-started")) {
57+
trackDocSection("Get Started");
58+
} else if (path.includes("/integrations")) {
59+
trackDocSection("Integrations");
60+
} else if (path.includes("/usecases")) {
61+
trackDocSection("Use Cases");
62+
} else if (path.includes("/updates")) {
63+
trackDocSection("Updates/Blog");
64+
}
65+
}, 100);
66+
67+
return () => clearTimeout(timer);
68+
}, [location.pathname]);
69+
70+
return <>{children}</>;
71+
}

src/utils/amplitude.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import * as amplitude from "@amplitude/analytics-browser";
2+
3+
// Configuration
4+
const AMPLITUDE_CONFIG = {
5+
fetchRemoteConfig: true,
6+
autocapture: {
7+
pageViews: true,
8+
sessions: true,
9+
formInteractions: true,
10+
fileDownloads: true,
11+
elementInteractions: true,
12+
},
13+
minIdLength: 5,
14+
flushIntervalMillis: 1000,
15+
logLevel:
16+
typeof window !== "undefined" && window.location?.hostname === "localhost"
17+
? "Debug"
18+
: "Warn",
19+
};
20+
21+
let isInitialized = false;
22+
23+
/**
24+
* Initialize Amplitude tracking
25+
* @param {string} apiKey - Amplitude API key
26+
* @param {string} [userId] - Optional user ID
27+
* @param {object} [options] - Additional configuration options
28+
*/
29+
export function initAmplitude(apiKey, userId = null, options = {}) {
30+
if (typeof window === "undefined" || isInitialized || !apiKey) {
31+
return;
32+
}
33+
34+
try {
35+
const config = { ...AMPLITUDE_CONFIG, ...options };
36+
37+
if (userId) {
38+
amplitude.init(apiKey, userId, config);
39+
} else {
40+
amplitude.init(apiKey, config);
41+
}
42+
43+
isInitialized = true;
44+
console.log("Amplitude initialized successfully");
45+
} catch (error) {
46+
console.error("Failed to initialize Amplitude:", error);
47+
}
48+
}
49+
50+
/**
51+
* Track a custom event
52+
* @param {string} eventName - Name of the event
53+
* @param {object} [properties] - Event properties
54+
*/
55+
export function trackEvent(eventName, properties = {}) {
56+
if (!isInitialized || typeof window === "undefined") {
57+
return;
58+
}
59+
60+
try {
61+
amplitude.track(eventName, {
62+
...properties,
63+
timestamp: Date.now(),
64+
page_url: window.location.href,
65+
page_title: document.title,
66+
});
67+
} catch (error) {
68+
console.error("Failed to track event:", error);
69+
}
70+
}
71+
72+
/**
73+
* Track page view
74+
* @param {string} [pageName] - Optional page name override
75+
* @param {object} [properties] - Additional properties
76+
*/
77+
export function trackPageView(pageName = null, properties = {}) {
78+
if (!isInitialized || typeof window === "undefined") {
79+
return;
80+
}
81+
82+
const page = pageName || document.title || window.location.pathname;
83+
84+
trackEvent("Page View", {
85+
page_name: page,
86+
page_path: window.location.pathname,
87+
page_url: window.location.href,
88+
referrer: document.referrer || "direct",
89+
...properties,
90+
});
91+
}
92+
93+
/**
94+
* Track documentation section visits
95+
* @param {string} section - Documentation section name
96+
* @param {object} [properties] - Additional properties
97+
*/
98+
export function trackDocSection(section, properties = {}) {
99+
trackEvent("Documentation Section Viewed", {
100+
section,
101+
...properties,
102+
});
103+
}
104+
105+
/**
106+
* Track search queries
107+
* @param {string} query - Search query
108+
* @param {number} [resultsCount] - Number of results
109+
* @param {object} [properties] - Additional properties
110+
*/
111+
export function trackSearch(query, resultsCount = null, properties = {}) {
112+
trackEvent("Search Performed", {
113+
search_query: query,
114+
results_count: resultsCount,
115+
...properties,
116+
});
117+
}
118+
119+
/**
120+
* Track link clicks
121+
* @param {string} linkUrl - URL of the clicked link
122+
* @param {string} [linkText] - Text of the link
123+
* @param {string} [linkType] - Type of link (internal, external, etc.)
124+
* @param {object} [properties] - Additional properties
125+
*/
126+
export function trackLinkClick(
127+
linkUrl,
128+
linkText = null,
129+
linkType = "unknown",
130+
properties = {},
131+
) {
132+
trackEvent("Link Clicked", {
133+
link_url: linkUrl,
134+
link_text: linkText,
135+
link_type: linkType,
136+
...properties,
137+
});
138+
}
139+
140+
/**
141+
* Track code example interactions
142+
* @param {string} language - Programming language
143+
* @param {string} action - Action performed (copy, run, etc.)
144+
* @param {object} [properties] - Additional properties
145+
*/
146+
export function trackCodeExample(language, action, properties = {}) {
147+
trackEvent("Code Example Interaction", {
148+
language,
149+
action,
150+
...properties,
151+
});
152+
}
153+
154+
/**
155+
* Set user properties
156+
* @param {object} properties - User properties to set
157+
*/
158+
export function setUserProperties(properties) {
159+
if (!isInitialized || typeof window === "undefined") {
160+
return;
161+
}
162+
163+
try {
164+
amplitude.identify(new amplitude.Identify().set(properties));
165+
} catch (error) {
166+
console.error("Failed to set user properties:", error);
167+
}
168+
}
169+
170+
/**
171+
* Set user ID
172+
* @param {string} userId - User ID
173+
*/
174+
export function setUserId(userId) {
175+
if (!isInitialized || typeof window === "undefined") {
176+
return;
177+
}
178+
179+
try {
180+
amplitude.setUserId(userId);
181+
} catch (error) {
182+
console.error("Failed to set user ID:", error);
183+
}
184+
}
185+
186+
/**
187+
* Reset user session (logout)
188+
*/
189+
export function resetUser() {
190+
if (!isInitialized || typeof window === "undefined") {
191+
return;
192+
}
193+
194+
try {
195+
amplitude.reset();
196+
} catch (error) {
197+
console.error("Failed to reset user:", error);
198+
}
199+
}
200+
201+
// Export amplitude instance for advanced usage
202+
export { amplitude };
203+
204+
// Check if we're in a browser environment and auto-initialize if API key is available
205+
if (typeof window !== "undefined") {
206+
// You can set the API key via environment variable or global variable
207+
const apiKey =
208+
process.env.REACT_APP_AMPLITUDE_API_KEY || window.AMPLITUDE_API_KEY;
209+
210+
if (apiKey) {
211+
// Initialize on page load
212+
if (document.readyState === "loading") {
213+
document.addEventListener("DOMContentLoaded", () => {
214+
initAmplitude(apiKey);
215+
});
216+
} else {
217+
initAmplitude(apiKey);
218+
}
219+
}
220+
}

0 commit comments

Comments
 (0)