Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions flexfoil-ui/src/components/panels/SolvePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { runSweep, type SweepConfig, type SweepRunData } from '../../lib/sweepEn
import type { PolarPoint, SweepAxis, SweepParam, ReType } from '../../types';
import type { RunInsert } from '../../lib/storageBackend';
import { parseSweepValues, formatSweepValues } from '../../lib/parseSweepValues';
import { trackEvent } from '../../lib/analytics';

type SolveOrCacheResult = {
result: AnalysisResult | null;
Expand Down Expand Up @@ -338,6 +339,12 @@ export function SolvePanel() {
return;
}

trackEvent('solve_run', {
solve_mode: runMode === 'alpha' ? 'single_alpha' : 'single_cl',
solver_mode: isViscous ? 'viscous' : 'inviscid',
n_panels: panels.length - 1,
});

const jobLabel = runMode === 'alpha'
? `${isViscous ? 'Viscous' : 'Inviscid'} @ alpha=${targetAlpha.toFixed(1)}`
: `${isViscous ? 'Viscous' : 'Inviscid'} -> CL=${targetCl.toFixed(3)}`;
Expand Down Expand Up @@ -579,6 +586,12 @@ export function SolvePanel() {

clearPolarSuppression();

trackEvent('solve_run', {
solve_mode: 'polar',
solver_mode: isViscous ? 'viscous' : 'inviscid',
n_panels: panels.length - 1,
});

const { id: jobId, signal } = jobDispatch(
`Polar ${alphaStart.toFixed(0)} to ${alphaEnd.toFixed(0)} (${isViscous ? 'viscous' : 'inviscid'})`
);
Expand Down Expand Up @@ -724,6 +737,12 @@ export function SolvePanel() {
const controller = new AbortController();
sweepAbortRef.current = controller;

trackEvent('solve_run', {
solve_mode: sweepSecondary ? 'sweep_2d' : 'sweep_1d',
solver_mode: isViscous ? 'viscous' : 'inviscid',
n_panels: panels.length - 1,
});

const sweepLabel = sweepSecondary
? `Sweep ${sweepPrimary.param} × ${sweepSecondary.param} (${isViscous ? 'viscous' : 'inviscid'})`
: `Sweep ${sweepPrimary.param} ${sweepPrimary.start}→${sweepPrimary.end} (${isViscous ? 'viscous' : 'inviscid'})`;
Expand Down
32 changes: 32 additions & 0 deletions flexfoil-ui/src/lib/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { trackEvent } from './analytics';

// Tests run in the node environment, so stand up the `window` the module targets.
const globals = globalThis as { window?: { gtag?: (...args: unknown[]) => void } };

beforeEach(() => {
globals.window = {};
});

afterEach(() => {
delete globals.window;
});

describe('trackEvent', () => {
it('forwards the event name and params to gtag', () => {
const gtag = vi.fn();
globals.window = { gtag };

trackEvent('solve_run', { solve_mode: 'polar', solver_mode: 'viscous', n_panels: 160 });

expect(gtag).toHaveBeenCalledWith('event', 'solve_run', {
solve_mode: 'polar',
solver_mode: 'viscous',
n_panels: 160,
});
});

it('does not throw when the tag is blocked', () => {
expect(() => trackEvent('solve_run')).not.toThrow();
});
});
8 changes: 8 additions & 0 deletions flexfoil-ui/src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ export function initAnalyticsConsent() {
}
}

/**
* Send a custom GA4 event. Consent Mode gates delivery, so callers do not need
* to check consent first. gtag is absent when the tag is blocked, hence optional.
*/
export function trackEvent(name: string, params?: Record<string, string | number | boolean>) {
window.gtag?.('event', name, params);
}

declare global {
interface Window {
gtag?: (...args: unknown[]) => void;
Expand Down
Loading