feat: add GA4 key events tracking (run_simulation, open_example, create_project, compile_code, visit_github)
Co-authored-by: davidmonterocrespo24 <47928504+davidmonterocrespo24@users.noreply.github.com>pull/37/head
parent
156a6159b7
commit
13ee547ad7
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Analytics Events
|
||||||
|
|
||||||
|
Velxio uses **Google Analytics 4 (GA4)** to measure key user interactions. Events are fired via `gtag` and should be marked as **Key Events** in the GA4 dashboard to track conversions and engagement.
|
||||||
|
|
||||||
|
The tracking utility lives in [`frontend/src/utils/analytics.ts`](../frontend/src/utils/analytics.ts).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Events
|
||||||
|
|
||||||
|
### `run_simulation`
|
||||||
|
|
||||||
|
Fired when the user clicks the **Run** button to start a simulation.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|--------------|
|
||||||
|
| `event_category` | `engagement` |
|
||||||
|
|
||||||
|
**Location:** `EditorToolbar.tsx` → `handleRun()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `open_example`
|
||||||
|
|
||||||
|
Fired when a user loads a sample project from the Examples gallery.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|------------------------|
|
||||||
|
| `event_category` | `engagement` |
|
||||||
|
| `event_label` | Title of the example |
|
||||||
|
|
||||||
|
**Location:** `ExamplesPage.tsx` → `handleLoadExample()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `create_project`
|
||||||
|
|
||||||
|
Fired when a user successfully saves a **new** project (not when updating an existing one).
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|--------------|
|
||||||
|
| `event_category` | `engagement` |
|
||||||
|
|
||||||
|
**Location:** `SaveProjectModal.tsx` → `handleSave()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `compile_code`
|
||||||
|
|
||||||
|
Fired when code compilation starts (user clicks the **Compile** button).
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|---------------|
|
||||||
|
| `event_category` | `development` |
|
||||||
|
|
||||||
|
**Location:** `EditorToolbar.tsx` → `handleCompile()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `visit_github`
|
||||||
|
|
||||||
|
Fired when a user clicks any link pointing to the Velxio GitHub repository.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|-----------------|
|
||||||
|
| `event_category` | `external_link` |
|
||||||
|
|
||||||
|
**Locations:**
|
||||||
|
- `LandingPage.tsx` — nav bar, hero CTA, and footer GitHub links
|
||||||
|
- `AppHeader.tsx` — editor header GitHub link
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
All helpers are exported from `frontend/src/utils/analytics.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
trackRunSimulation,
|
||||||
|
trackOpenExample,
|
||||||
|
trackCreateProject,
|
||||||
|
trackCompileCode,
|
||||||
|
trackVisitGitHub,
|
||||||
|
} from '../utils/analytics';
|
||||||
|
```
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Fire an event
|
||||||
|
trackRunSimulation();
|
||||||
|
|
||||||
|
// Fire an event with a label
|
||||||
|
trackOpenExample('Blink LED');
|
||||||
|
```
|
||||||
|
|
||||||
|
The module safely checks whether `gtag` is available before calling it, so it does not throw errors in environments where the GA script is not loaded (e.g., local development without the GA tag).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Marking Events as Key Events in GA4
|
||||||
|
|
||||||
|
1. Open **Google Analytics → Admin → Events**.
|
||||||
|
2. Locate the event by name (e.g. `run_simulation`).
|
||||||
|
3. Toggle **Mark as key event** to enable conversion tracking.
|
||||||
|
|
||||||
|
Repeat for each event listed above.
|
||||||
|
|
@ -7,6 +7,7 @@ import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
|
||||||
import { parseCompileResult } from '../../utils/compilationLogger';
|
import { parseCompileResult } from '../../utils/compilationLogger';
|
||||||
import type { CompilationLog } from '../../utils/compilationLogger';
|
import type { CompilationLog } from '../../utils/compilationLogger';
|
||||||
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
|
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
|
||||||
|
import { trackCompileCode, trackRunSimulation } from '../../utils/analytics';
|
||||||
import './EditorToolbar.css';
|
import './EditorToolbar.css';
|
||||||
|
|
||||||
interface EditorToolbarProps {
|
interface EditorToolbarProps {
|
||||||
|
|
@ -43,6 +44,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
setCompiling(true);
|
setCompiling(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
setConsoleOpen(true);
|
setConsoleOpen(true);
|
||||||
|
trackCompileCode();
|
||||||
|
|
||||||
const fqbn = BOARD_FQBN[boardType];
|
const fqbn = BOARD_FQBN[boardType];
|
||||||
const boardLabel = BOARD_LABELS[boardType];
|
const boardLabel = BOARD_LABELS[boardType];
|
||||||
|
|
@ -81,6 +83,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
|
|
||||||
const handleRun = () => {
|
const handleRun = () => {
|
||||||
if (compiledHex) {
|
if (compiledHex) {
|
||||||
|
trackRunSimulation();
|
||||||
startSimulation();
|
startSimulation();
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../store/useAuthStore';
|
import { useAuthStore } from '../../store/useAuthStore';
|
||||||
|
import { trackVisitGitHub } from '../../utils/analytics';
|
||||||
|
|
||||||
interface AppHeaderProps {}
|
interface AppHeaderProps {}
|
||||||
|
|
||||||
|
|
@ -55,6 +56,7 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title="GitHub — Velxio"
|
title="GitHub — Velxio"
|
||||||
|
onClick={trackVisitGitHub}
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 5, color: '#ccc', textDecoration: 'none', fontSize: 13 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 5, color: '#ccc', textDecoration: 'none', fontSize: 13 }}
|
||||||
>
|
>
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEditorStore } from '../../store/useEditorStore';
|
||||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||||
import { useProjectStore } from '../../store/useProjectStore';
|
import { useProjectStore } from '../../store/useProjectStore';
|
||||||
import { createProject, updateProject } from '../../services/projectService';
|
import { createProject, updateProject } from '../../services/projectService';
|
||||||
|
import { trackCreateProject } from '../../utils/analytics';
|
||||||
|
|
||||||
interface SaveProjectModalProps {
|
interface SaveProjectModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -56,6 +57,7 @@ export const SaveProjectModal: React.FC<SaveProjectModalProps> = ({ onClose }) =
|
||||||
saved = await updateProject(currentProject.id, payload);
|
saved = await updateProject(currentProject.id, payload);
|
||||||
} else {
|
} else {
|
||||||
saved = await createProject(payload);
|
saved = await createProject(payload);
|
||||||
|
trackCreateProject();
|
||||||
}
|
}
|
||||||
setCurrentProject({
|
setCurrentProject({
|
||||||
id: saved.id,
|
id: saved.id,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ExamplesGallery } from '../components/examples/ExamplesGallery';
|
||||||
import { useEditorStore } from '../store/useEditorStore';
|
import { useEditorStore } from '../store/useEditorStore';
|
||||||
import { useSimulatorStore } from '../store/useSimulatorStore';
|
import { useSimulatorStore } from '../store/useSimulatorStore';
|
||||||
import type { ExampleProject } from '../data/examples';
|
import type { ExampleProject } from '../data/examples';
|
||||||
|
import { trackOpenExample } from '../utils/analytics';
|
||||||
|
|
||||||
export const ExamplesPage: React.FC = () => {
|
export const ExamplesPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -18,6 +19,7 @@ export const ExamplesPage: React.FC = () => {
|
||||||
|
|
||||||
const handleLoadExample = (example: ExampleProject) => {
|
const handleLoadExample = (example: ExampleProject) => {
|
||||||
console.log('Loading example:', example.title);
|
console.log('Loading example:', example.title);
|
||||||
|
trackOpenExample(example.title);
|
||||||
|
|
||||||
// Switch board type if the example specifies one
|
// Switch board type if the example specifies one
|
||||||
const targetBoard = example.boardType || 'arduino-uno';
|
const targetBoard = example.boardType || 'arduino-uno';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../store/useAuthStore';
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
import { trackVisitGitHub } from '../utils/analytics';
|
||||||
import './LandingPage.css';
|
import './LandingPage.css';
|
||||||
|
|
||||||
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
|
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
|
||||||
|
|
@ -417,7 +418,7 @@ export const LandingPage: React.FC = () => {
|
||||||
<span>Velxio</span>
|
<span>Velxio</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="landing-nav-links">
|
<div className="landing-nav-links">
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="nav-link">
|
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" onClick={trackVisitGitHub} className="nav-link">
|
||||||
<IcoGitHub /> GitHub
|
<IcoGitHub /> GitHub
|
||||||
</a>
|
</a>
|
||||||
<Link to="/docs" className="nav-link">Docs</Link>
|
<Link to="/docs" className="nav-link">Docs</Link>
|
||||||
|
|
@ -458,7 +459,7 @@ export const LandingPage: React.FC = () => {
|
||||||
<IcoZap />
|
<IcoZap />
|
||||||
Launch Editor
|
Launch Editor
|
||||||
</Link>
|
</Link>
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="cta-secondary">
|
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" onClick={trackVisitGitHub} className="cta-secondary">
|
||||||
<IcoGitHub />
|
<IcoGitHub />
|
||||||
View on GitHub
|
View on GitHub
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -564,7 +565,7 @@ export const LandingPage: React.FC = () => {
|
||||||
<span>Velxio</span>
|
<span>Velxio</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-links">
|
<div className="footer-links">
|
||||||
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer">GitHub</a>
|
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" onClick={trackVisitGitHub}>GitHub</a>
|
||||||
<Link to="/docs">Docs</Link>
|
<Link to="/docs">Docs</Link>
|
||||||
<Link to="/examples">Examples</Link>
|
<Link to="/examples">Examples</Link>
|
||||||
<Link to="/editor">Editor</Link>
|
<Link to="/editor">Editor</Link>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Google Analytics 4 Key Events Tracking
|
||||||
|
*
|
||||||
|
* Provides helper functions to fire GA4 custom events for key user actions.
|
||||||
|
* Each function maps to an event that should be marked as a Key Event in GA4.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare function gtag(command: 'event', eventName: string, eventParams?: Record<string, unknown>): void;
|
||||||
|
|
||||||
|
function fireEvent(eventName: string, params: Record<string, string | number | boolean>): void {
|
||||||
|
if (typeof gtag === 'function') {
|
||||||
|
gtag('event', eventName, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fired when the user starts a simulation (clicks Run). */
|
||||||
|
export function trackRunSimulation(): void {
|
||||||
|
fireEvent('run_simulation', { event_category: 'engagement' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fired when a user loads a sample project from the examples gallery. */
|
||||||
|
export function trackOpenExample(exampleTitle?: string): void {
|
||||||
|
fireEvent('open_example', {
|
||||||
|
event_category: 'engagement',
|
||||||
|
...(exampleTitle ? { event_label: exampleTitle } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fired when a user successfully creates a new project. */
|
||||||
|
export function trackCreateProject(): void {
|
||||||
|
fireEvent('create_project', { event_category: 'engagement' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fired when code compilation starts. */
|
||||||
|
export function trackCompileCode(): void {
|
||||||
|
fireEvent('compile_code', { event_category: 'development' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fired when a user clicks any GitHub repository link. */
|
||||||
|
export function trackVisitGitHub(): void {
|
||||||
|
fireEvent('visit_github', { event_category: 'external_link' });
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue