diff --git a/docs/analytics-custom-events.md b/docs/analytics-custom-events.md index 54e5947..bc8e36a 100644 --- a/docs/analytics-custom-events.md +++ b/docs/analytics-custom-events.md @@ -46,16 +46,28 @@ los usuarios usan el contenido. ## Implementación - Crear utilidad `src/services/analytics.js` con helper `trackEvent(name, params)` - que llame a `window.gtag`. + que llame a `window.gtag('event', name, params)`. - Integrar la llamada en cada punto indicado. - Verificar en GA4 > Realtime que los eventos aparezcan. +### Referencias + +- **Cómo enviar eventos desde código (gtag.js)**: [Google tag (gtag.js) - Reference + events](https://developers.google.com/tag-platform/gtagjs/reference/events) + — documenta `gtag('event', '', { : })`. +- **Eventos en GA4 (recomendados, personalizados y parámetros)**: + [Events - Analytics Help](https://support.google.com/analytics/answer/12229021). +- **Marcar eventos como key event / conversión (paso final, sin código)**: + [Create or modify key events](https://support.google.com/analytics/answer/12844695) + — útil solo para convertir `contact_click` o `download_cv` en conversión + desde Admin > Events, **no** para enviar los eventos. + ## Criterios de aceptación -- [ ] Helper `trackEvent` en `src/services/analytics.js` -- [ ] `language_changed` con parámetro `language` -- [ ] `project_open` con `company` y `project` -- [ ] `contact_click` con `network` -- [ ] `download_cv` con `language` -- [ ] `about_open` +- [x] Helper `trackEvent` en `src/services/analytics.js` +- [x] `language_changed` con parámetro `language` +- [x] `project_open` con `company` y `project` +- [x] `contact_click` con `network` +- [x] `download_cv` con `language` +- [x] `about_open` - [ ] Eventos visibles en GA4 Realtime tras deploy diff --git a/index.html b/index.html index e50e789..e5067fb 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@ /> - Raul Jonatan | Full stack engineering + Raul Jonatan | Full Stack Engineer diff --git a/package.json b/package.json index 814f404..f4d2845 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,10 @@ "eslint-plugin-react-refresh": "^0.4.19", "postcss": "^8.5.3", "tailwindcss": "^3.4.17", - "vite": "^5.4.14" + "vite": "^5.4.14", + "vite-plugin-prerender": "^1.0.8" + }, + "overrides": { + "puppeteer": "^21.11.0" } } diff --git a/src/App.jsx b/src/App.jsx index 512b0e9..77b90cb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -13,6 +13,7 @@ import PopupLanguageSelector from "./components/features/popupLanguageSelector"; import AboutPopup from "./components/core/aboutPopup"; import { menuStore } from "./stores/menuStore"; +import { trackEvent } from "./services/analytics"; function App() { const { t, i18n } = useTranslation(); @@ -32,6 +33,12 @@ function App() { }; const toggle = (workExperience) => { + if (workExperience) { + trackEvent("project_open", { + company: workExperience.companyName, + project: workExperience.proyect, + }); + } setIsOpen((isOpen) => !isOpen); setWorkExperience(workExperience); setIsContactOpen(false); @@ -41,6 +48,9 @@ function App() { const menuSelected = (subItemSelected) => { const menuAction = findMenuActionBySubItem(subItemSelected); if (menuAction.type === "POPUP") { + if (menuAction.subMenuTitle === "About") { + trackEvent("about_open"); + } setSubMenuItem(menuAction); } else if (menuAction && menuAction.type === "LINK") { menuAction.component(i18n.language); @@ -57,21 +67,27 @@ function App() { return (
{/* Top Menu Bar */} - +
+ +
{/* Main Content */} -
+
+

Raul Jonatan | Software Developer

-
+
[ -

e.key === "Enter" && onContactToggle()} > RAUL JONATAN -

+ ] |

Full stack engineering

{t("main.specialty")}

{t("main.working")}

-
+
+

{t("workExperience.title")}

    @@ -116,7 +133,9 @@ function App() { ))}
+
+

{t("academicTraining.title")}

    @@ -130,14 +149,17 @@ function App() {
+
{isContactOpen && } -
+ {/* Status Bar */} - +
+ +
{isOpen && } {subMenuItem && subMenuItem.subMenuTitle === "About" && ( diff --git a/src/components/core/aboutPopup.jsx b/src/components/core/aboutPopup.jsx index 4dadcaf..2d3b114 100644 --- a/src/components/core/aboutPopup.jsx +++ b/src/components/core/aboutPopup.jsx @@ -33,6 +33,7 @@ export default function AboutPopup({ onClose }) { React & Tailwind diff --git a/src/components/features/contact.jsx b/src/components/features/contact.jsx index 1ef0ce4..e67abc3 100644 --- a/src/components/features/contact.jsx +++ b/src/components/features/contact.jsx @@ -1,7 +1,9 @@ import { useTranslation } from "react-i18next"; +import { trackEvent } from "../../services/analytics"; export default function Contact({ onClose }) { const { t } = useTranslation(); + const onContactClick = (network) => trackEvent('contact_click', { network }); return ( diff --git a/src/components/features/popupLanguageSelector.jsx b/src/components/features/popupLanguageSelector.jsx index df28a77..9abc05c 100644 --- a/src/components/features/popupLanguageSelector.jsx +++ b/src/components/features/popupLanguageSelector.jsx @@ -1,11 +1,13 @@ import { useTranslation } from "react-i18next"; import Button from "../core/button"; +import { trackEvent } from "../../services/analytics"; export default function PopupLanguageSelector({ onClose, languageChanged}) { const { i18n } = useTranslation(); const languages = [{ value: 'en', name: 'English' }, { value: 'es', name: 'Spanish' }] function languagesOnChange({ target }) { i18n.changeLanguage(target.value); + trackEvent('language_changed', { language: target.value }); languageChanged() } diff --git a/src/constants/popup.js b/src/constants/popup.js index 7b09848..fa513b2 100644 --- a/src/constants/popup.js +++ b/src/constants/popup.js @@ -1,5 +1,6 @@ import AboutPopup from "../components/core/aboutPopup"; import PopupLanguageSelector from "../components/features/popupLanguageSelector"; +import { trackEvent } from "../services/analytics"; export const SUBMENU_ACTIONS = [ { @@ -14,6 +15,7 @@ export const SUBMENU_ACTIONS = [ type: 'LINK', component: (lng) => { const fileName = lng && lng === 'es' ? 'Profile_es.pdf' : 'Profile.pdf' + trackEvent('download_cv', { language: lng && lng === 'es' ? 'es' : 'en' }) const a = document.createElement("a"); a.href = `documents/${fileName}`; a.target = "_blank"; diff --git a/src/main.jsx b/src/main.jsx index f25cdd1..a0e9253 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,22 @@ import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import { createRoot, hydrateRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' -import './i18n'; -createRoot(document.getElementById('root')).render( - - - , -) +import './i18n' + +const rootElement = document.getElementById('root') + +if (rootElement.hasChildNodes()) { + hydrateRoot( + rootElement, + + + , + ) +} else { + createRoot(rootElement).render( + + + , + ) +} diff --git a/src/services/analytics.js b/src/services/analytics.js new file mode 100644 index 0000000..ca5ec45 --- /dev/null +++ b/src/services/analytics.js @@ -0,0 +1,5 @@ +export const trackEvent = (eventName, params = {}) => { + if (typeof window === "undefined") return; + if (typeof window.gtag !== "function") return; + window.gtag("event", eventName, params); +}; diff --git a/vite.config.cjs b/vite.config.cjs new file mode 100644 index 0000000..effee68 --- /dev/null +++ b/vite.config.cjs @@ -0,0 +1,26 @@ +const { defineConfig } = require('vite') +const react = require('@vitejs/plugin-react') +const vitePrerender = require('vite-plugin-prerender') +const fs = require('fs') +const path = require('path') + +const systemChromePath = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' + +module.exports = defineConfig({ + plugins: [ + react(), + vitePrerender({ + staticDir: path.resolve('dist'), + routes: ['/'], + renderer: new vitePrerender.PuppeteerRenderer({ + executablePath: + process.platform === 'darwin' && fs.existsSync(systemChromePath) + ? systemChromePath + : undefined, + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox'], + skipThirdPartyRequests: true, + }), + }), + ], +}) \ No newline at end of file diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 3e34260..0000000 --- a/vite.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -export default defineConfig({ - plugins: [react()], -}) \ No newline at end of file