From 0420eb2fdb4f3589ba7a30c5d14baeb93d04c245 Mon Sep 17 00:00:00 2001 From: Viresh Soedhwa Date: Mon, 25 May 2026 21:07:51 -0700 Subject: [PATCH 1/2] feat: add OpenTelemetry browser analytics with OTLP log export --- conf.d/default.conf | 11 ++ docusaurus.config.ts | 1 + package-lock.json | 262 +++++++++++++++++++++++++++++ package.json | 7 + src/analytics/AnalyticsContext.tsx | 20 +++ src/analytics/clientModule.ts | 13 ++ src/analytics/init.ts | 66 ++++++++ src/analytics/useAnalytics.ts | 52 ++++++ src/theme/Root.tsx | 15 ++ 9 files changed, 447 insertions(+) create mode 100644 src/analytics/AnalyticsContext.tsx create mode 100644 src/analytics/clientModule.ts create mode 100644 src/analytics/init.ts create mode 100644 src/analytics/useAnalytics.ts create mode 100644 src/theme/Root.tsx diff --git a/conf.d/default.conf b/conf.d/default.conf index b0bd1a4..9d6e148 100644 --- a/conf.d/default.conf +++ b/conf.d/default.conf @@ -94,6 +94,17 @@ server { try_files $uri $uri/ =404; } + # Analytics: proxy browser OTLP log requests to the in-cluster collector + location = /v1/logs { + proxy_pass http://opentelemetry-collector.observability.svc:4318/v1/logs; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Content-Type $content_type; + client_max_body_size 64k; + access_log off; + } + # Docusaurus SPA fallback location / { try_files $uri $uri/ /index.html; diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 551edce..1da4fad 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -5,6 +5,7 @@ import type * as Preset from '@docusaurus/preset-classic'; // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) const config: Config = { + clientModules: ['./src/analytics/clientModule.ts'], title: 'Open Learning Data', tagline: 'An open data portal for learning analytics research.', favicon: 'img/favicon.ico', diff --git a/package-lock.json b/package-lock.json index 9aa746a..8c9c36f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,13 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@mdx-js/react": "^3.0.0", + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/browser-instrumentation": "^0.5.2", + "@opentelemetry/exporter-logs-otlp-http": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/resources": "^2.7.1", + "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.41.1", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", @@ -4963,6 +4970,206 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", + "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/browser-instrumentation": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/browser-instrumentation/-/browser-instrumentation-0.5.2.tgz", + "integrity": "sha512-CoTyQCERrzJQKkPqMBg2+6gTBt0ZfF28hzzHxRQG5xLnZfIlhd91IPPsYPzk5LWDi9vkAk6NVwZJmGjDYt2iAg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/core": "^2.7.1", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.41.1", + "web-vitals": "^5.2.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", + "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.218.0.tgz", + "integrity": "sha512-Qx+4rpVHzgg89dawcWRHyt+XRXeLnhFz/qBtvggmjkcgPUdr+NAB0/u/eIPA8yAeJV0J80Vz43JZCh/XFvZFGw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-exporter-base": "0.218.0", + "@opentelemetry/otlp-transformer": "0.218.0", + "@opentelemetry/sdk-logs": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.218.0.tgz", + "integrity": "sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", + "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/otlp-transformer": "0.218.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", + "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/sdk-logs": "0.218.0", + "@opentelemetry/sdk-metrics": "2.7.1", + "@opentelemetry/sdk-trace-base": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", + "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.218.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", + "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.218.0", + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", + "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", + "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.7.1", + "@opentelemetry/resources": "2.7.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.129.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", @@ -6782,6 +6989,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-import-phases": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", @@ -7898,6 +8114,12 @@ "node": ">=8" } }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -11766,6 +11988,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", + "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/import-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", @@ -15614,6 +15851,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -18843,6 +19086,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/require-like": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", @@ -21385,6 +21641,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-vitals": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", + "license": "Apache-2.0" + }, "node_modules/webpack": { "version": "5.105.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", diff --git a/package.json b/package.json index 8e92afa..bcda69e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,13 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@mdx-js/react": "^3.0.0", + "@opentelemetry/api-logs": "^0.218.0", + "@opentelemetry/browser-instrumentation": "^0.5.2", + "@opentelemetry/exporter-logs-otlp-http": "^0.218.0", + "@opentelemetry/instrumentation": "^0.218.0", + "@opentelemetry/resources": "^2.7.1", + "@opentelemetry/sdk-logs": "^0.218.0", + "@opentelemetry/semantic-conventions": "^1.41.1", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", diff --git a/src/analytics/AnalyticsContext.tsx b/src/analytics/AnalyticsContext.tsx new file mode 100644 index 0000000..74f46a7 --- /dev/null +++ b/src/analytics/AnalyticsContext.tsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext, useMemo } from 'react'; +import { useAnalytics } from './useAnalytics'; + +const AnalyticsContext = createContext<{ trackEvent: (name: string, attrs?: Record) => void }>({ + trackEvent: () => {}, +}); + +export function AnalyticsProvider({ children }: { children: React.ReactNode }) { + const analytics = useAnalytics(); + const value = useMemo(() => analytics, [analytics]); + return ( + + {children} + + ); +} + +export function useTrackEvent() { + return useContext(AnalyticsContext).trackEvent; +} diff --git a/src/analytics/clientModule.ts b/src/analytics/clientModule.ts new file mode 100644 index 0000000..5ea0bf2 --- /dev/null +++ b/src/analytics/clientModule.ts @@ -0,0 +1,13 @@ +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + +if (ExecutionEnvironment.canUseDOM) { + import('./init').then(({ initAnalytics }) => initAnalytics()); +} + +export function onRouteDidUpdate({ location }: { location: { pathname: string } }): void { + if (ExecutionEnvironment.canUseDOM) { + import('./init').then(({ logEvent }) => { + logEvent('page_view', { url: location.pathname }); + }); + } +} diff --git a/src/analytics/init.ts b/src/analytics/init.ts new file mode 100644 index 0000000..6921164 --- /dev/null +++ b/src/analytics/init.ts @@ -0,0 +1,66 @@ +import { logs, SeverityNumber } from '@opentelemetry/api-logs'; +import { + LoggerProvider, + BatchLogRecordProcessor, + SimpleLogRecordProcessor, + ConsoleLogRecordExporter, +} from '@opentelemetry/sdk-logs'; +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { NavigationInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation'; +import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/navigation-timing'; +import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action'; +import { WebVitalsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/web-vitals'; +import { ErrorsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/errors'; + +const VERSION = require('../../package.json').version; + +export function initAnalytics(): void { + const isProduction = process.env.NODE_ENV === 'production'; + + const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'open-data', + [ATTR_SERVICE_VERSION]: VERSION, + }); + + const logExporter = isProduction + ? new OTLPLogExporter({ url: '/v1/logs' }) + : new ConsoleLogRecordExporter(); + + const processor = isProduction + ? new BatchLogRecordProcessor(logExporter) + : new SimpleLogRecordProcessor(logExporter); + + const loggerProvider = new LoggerProvider({ + resource, + processors: [processor], + }); + + logs.setGlobalLoggerProvider(loggerProvider); + + registerInstrumentations({ + instrumentations: [ + new NavigationInstrumentation(), + new NavigationTimingInstrumentation(), + new UserActionInstrumentation(), + new WebVitalsInstrumentation(), + new ErrorsInstrumentation(), + ], + }); +} + +export function logEvent(eventName: string, attributes: Record = {}): void { + try { + const logger = logs.getLogger('analytics'); + logger.emit({ + body: eventName, + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + attributes: { 'event.name': eventName, ...attributes }, + }); + } catch { + // Analytics must never break the UI + } +} diff --git a/src/analytics/useAnalytics.ts b/src/analytics/useAnalytics.ts new file mode 100644 index 0000000..41d8753 --- /dev/null +++ b/src/analytics/useAnalytics.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { logEvent } from './init'; + +function getSessionId(): string { + let id = sessionStorage.getItem('analytics_session_id'); + if (!id) { + id = crypto.randomUUID(); + sessionStorage.setItem('analytics_session_id', id); + } + return id; +} + +const commonAttributes = { + user_agent: navigator.userAgent, + screen_resolution: `${screen.width}x${screen.height}`, + referrer: document.referrer || '', +}; + +export function useAnalytics() { + const sessionId = useRef(getSessionId()); + const startTime = useRef(Date.now()); + + const trackEvent = useCallback((eventName: string, attributes: Record = {}) => { + logEvent(eventName, { + 'session.id': sessionId.current, + ...commonAttributes, + ...attributes, + }); + }, []); + + const trackPageView = useCallback(() => { + trackEvent('page_view', { + url: window.location.href, + }); + }, [trackEvent]); + + useEffect(() => { + trackEvent('session_start', { timestamp: new Date().toISOString() }); + + const interval = setInterval(() => { + if (!document.hidden) { + trackEvent('session_heartbeat', { + duration_seconds: Math.round((Date.now() - startTime.current) / 1000), + }); + } + }, 60_000); + + return () => clearInterval(interval); + }, [trackEvent]); + + return { trackEvent, trackPageView }; +} diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 0000000..5fec5ba --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +function AnalyticsWrapper({ children }: { children: React.ReactNode }) { + const { AnalyticsProvider } = require('../analytics/AnalyticsContext'); + return {children}; +} + +export default function Root({ children }: { children: React.ReactNode }) { + return ( + {children}}> + {() => {children}} + + ); +} From c6089e002b7d41f98e62db08dd4e9ffa559caaee Mon Sep 17 00:00:00 2001 From: Viresh Soedhwa Date: Mon, 25 May 2026 21:21:30 -0700 Subject: [PATCH 2/2] refactor: replace require() with ES6 imports and optimize Root component --- src/analytics/init.ts | 3 +-- src/theme/Root.tsx | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/analytics/init.ts b/src/analytics/init.ts index 6921164..e4663fb 100644 --- a/src/analytics/init.ts +++ b/src/analytics/init.ts @@ -14,8 +14,7 @@ import { NavigationTimingInstrumentation } from '@opentelemetry/browser-instrume import { UserActionInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/user-action'; import { WebVitalsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/web-vitals'; import { ErrorsInstrumentation } from '@opentelemetry/browser-instrumentation/experimental/errors'; - -const VERSION = require('../../package.json').version; +import { version as VERSION } from '../../package.json'; export function initAnalytics(): void { const isProduction = process.env.NODE_ENV === 'production'; diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx index 5fec5ba..4f26c28 100644 --- a/src/theme/Root.tsx +++ b/src/theme/Root.tsx @@ -1,15 +1,18 @@ -import React from 'react'; -import BrowserOnly from '@docusaurus/BrowserOnly'; +import React, { lazy, Suspense } from 'react'; +import useIsBrowser from '@docusaurus/useIsBrowser'; -function AnalyticsWrapper({ children }: { children: React.ReactNode }) { - const { AnalyticsProvider } = require('../analytics/AnalyticsContext'); - return {children}; -} +const AnalyticsProvider = lazy(() => + import('../analytics/AnalyticsContext').then((m) => ({ default: m.AnalyticsProvider })) +); export default function Root({ children }: { children: React.ReactNode }) { + const isBrowser = useIsBrowser(); + if (!isBrowser) { + return <>{children}; + } return ( - {children}}> - {() => {children}} - + {children}}> + {children} + ); }