diff --git a/apps/demo/PLAUSIBLE_SETUP.md b/apps/demo/PLAUSIBLE_SETUP.md new file mode 100644 index 000000000..61364ba47 --- /dev/null +++ b/apps/demo/PLAUSIBLE_SETUP.md @@ -0,0 +1,221 @@ +# Plausible Analytics Setup Guide + +This guide walks you through setting up Plausible Analytics with a Cloudflare Worker proxy for the pgflow demo app. + +## Overview + +The setup consists of three parts: +1. Plausible dashboard configuration +2. Cloudflare Worker proxy deployment +3. SvelteKit app configuration + +## Part 1: Plausible Dashboard Setup + +1. **Sign up or log in** to [Plausible](https://plausible.io) + +2. **Add your website** + - Click "Add a website" + - Enter your domain (e.g., `demo.pgflow.dev`) + - Click "Add site" + +3. **Get your script URL** + - Go to Site Settings > General > Site Installation + - Find your unique script URL - it will look like: + ``` + https://plausible.io/js/pa-XXXXX.js + ``` + - **Save this URL** - you'll need it for the Cloudflare Worker + +## Part 2: Cloudflare Worker Setup + +### Step 1: Create the Worker + +1. Go to your [Cloudflare Dashboard](https://dash.cloudflare.com) +2. Navigate to "Workers & Pages" in the sidebar +3. Click "Create" > "Create Worker" +4. Give it a name (avoid words like 'analytics', 'tracking', 'stats') + - Good examples: `proxy-service`, `data-relay`, `metrics-hub` +5. Click "Deploy" + +### Step 2: Configure the Worker + +1. Click "Edit Code" +2. Delete the default code +3. Copy the code from `cloudflare-worker-plausible-proxy.js` +4. **Update the configuration** at the top of the file: + ```javascript + // Replace with your Plausible script URL from Part 1 + const ProxyScript = 'https://plausible.io/js/pa-XXXXX.js'; + + // Customize these paths (avoid obvious names) + const ScriptName = '/metrics/script.js'; // Change to something unique + const Endpoint = '/metrics/event'; // Should match folder above + ``` +5. Click "Save and Deploy" + +### Step 3: Test the Worker + +1. Your worker will be available at: + ``` + https://your-worker-name.your-account.workers.dev + ``` + +2. Test if the script is accessible: + ``` + https://your-worker-name.your-account.workers.dev/metrics/script.js + ``` + You should see JavaScript code (the Plausible script) + +### Step 4 (Optional): Add a Custom Route + +If your site is on Cloudflare CDN, you can run the proxy as a subdirectory: + +1. In the Worker settings, go to "Triggers" > "Routes" +2. Click "Add route" +3. Configure: + - Route: `*yourdomain.com/analytics/*` (change "analytics" to something else) + - Zone: Select your domain +4. Click "Save" + +Now your proxy will be available at: +``` +https://yourdomain.com/analytics/metrics/script.js +https://yourdomain.com/analytics/metrics/event +``` + +## Part 3: SvelteKit App Configuration + +### Update the Layout File + +Open `apps/demo/src/routes/+layout.svelte` and update the TODO section: + +```typescript +onMount(() => { + initPlausible({ + domain: 'demo.pgflow.dev', // Your actual domain + apiHost: 'https://your-worker.workers.dev/metrics', // Your proxy URL + trackLocalhost: false // Set to true for testing locally + }); +}); +``` + +**Configuration options:** + +- **Without custom route** (worker URL): + ```typescript + apiHost: 'https://your-worker-name.your-account.workers.dev/metrics' + ``` + +- **With custom route** (subdirectory): + ```typescript + apiHost: '/analytics/metrics' // Relative path works! + ``` + +## Part 4: Track Custom Events + +Use the `track()` function anywhere in your SvelteKit app: + +```typescript +import { track } from '$lib/analytics'; + +// Simple event +track('button_clicked'); + +// Event with properties +track('signup', { + tier: 'pro', + plan: 'monthly', + source: 'landing_page' +}); + +// Event with revenue tracking +track('purchase', { + product: 'pro-plan', + quantity: 1 +}, { + amount: 29.99, + currency: 'USD' +}); +``` + +### Common Event Examples + +```typescript +// User signup +track('signup', { method: 'email' }); + +// Feature usage +track('flow_created', { flow_type: 'data_pipeline' }); + +// Form submission +track('contact_form', { page: 'pricing' }); + +// Download tracking (already automatic with fileDownloads: true) +// But you can track custom downloads: +track('documentation_download', { doc_type: 'api_reference' }); +``` + +## Part 5: Verify Installation + +1. **Start your dev server**: + ```bash + pnpm nx dev demo + ``` + +2. **Open your browser console** and look for: + ``` + [Plausible] Initialized successfully + ``` + +3. **Check Plausible dashboard**: + - Go to your Plausible dashboard + - You should see pageviews appearing in real-time + - Note: It may take a few seconds for events to appear + +4. **Test custom events**: + ```typescript + // In browser console or your code + track('test_event', { test: true }); + ``` + - Check Plausible dashboard > Custom Events to see it + +## Troubleshooting + +### Events not showing up + +1. Check browser console for errors +2. Verify the proxy worker is accessible +3. Check that `domain` in config matches your Plausible site exactly +4. Make sure you're not on localhost (unless `trackLocalhost: true`) + +### Worker not accessible + +1. Verify the worker is deployed (check Cloudflare dashboard) +2. Check the worker logs for errors +3. Test the script URL directly in your browser + +### Ad blocker blocking requests + +1. Make sure you're using the proxy (not direct Plausible URLs) +2. Avoid obvious path names in your worker configuration +3. Use a custom route on your own domain + +## Production Checklist + +- [ ] Updated `ProxyScript` in worker with your Plausible script URL +- [ ] Customized `ScriptName` and `Endpoint` to avoid detection +- [ ] Deployed Cloudflare Worker successfully +- [ ] Tested worker script is accessible +- [ ] Updated `domain` in `+layout.svelte` with production domain +- [ ] Updated `apiHost` in `+layout.svelte` with worker URL +- [ ] Set `trackLocalhost: false` for production +- [ ] Verified events appear in Plausible dashboard +- [ ] Tested custom event tracking + +## Additional Resources + +- [Plausible Documentation](https://plausible.io/docs) +- [Plausible NPM Package](https://www.npmjs.com/package/@plausible-analytics/tracker) +- [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) +- [Custom Events Guide](https://plausible.io/docs/custom-event-goals) +- [Revenue Tracking Guide](https://plausible.io/docs/ecommerce-revenue-tracking) diff --git a/apps/demo/cloudflare-worker-plausible-proxy.js b/apps/demo/cloudflare-worker-plausible-proxy.js new file mode 100644 index 000000000..81d7c9a62 --- /dev/null +++ b/apps/demo/cloudflare-worker-plausible-proxy.js @@ -0,0 +1,51 @@ +/** + * Cloudflare Worker for Plausible Analytics Proxy + * + * IMPORTANT: You MUST update the ProxyScript variable below with your + * actual Plausible script URL from your dashboard before deploying! + */ + +// CONFIGURATION - UPDATE THIS VALUE! +// Get this from your Plausible dashboard (Site Settings > Site Installation) +// It will look like: https://plausible.io/js/script.js (or pa-XXXXX.js for custom domains) +const ProxyScript = 'https://plausible.io/js/script.js'; // ← UPDATE THIS! + +// Customize these paths to avoid ad-blocker detection +const ScriptName = '/stats/script.js'; +const Endpoint = '/stats/event'; + +// Internal logic - do not edit below +const ScriptWithoutExtension = ScriptName.replace('.js', ''); + +addEventListener('fetch', (event) => { + event.passThroughOnException(); + event.respondWith(handleRequest(event)); +}); + +async function handleRequest(event) { + const pathname = new URL(event.request.url).pathname; + const [baseUri, ...extensions] = pathname.split('.'); + + if (baseUri.endsWith(ScriptWithoutExtension)) { + return getScript(event, extensions); + } else if (pathname.endsWith(Endpoint)) { + return postData(event); + } + + return new Response(null, { status: 404 }); +} + +async function getScript(event, extensions) { + let response = await caches.default.match(event.request); + if (!response) { + response = await fetch(ProxyScript); + event.waitUntil(caches.default.put(event.request, response.clone())); + } + return response; +} + +async function postData(event) { + const request = new Request(event.request); + request.headers.delete('cookie'); + return await fetch('https://plausible.io/api/event', request); +} diff --git a/apps/demo/package.json b/apps/demo/package.json index b90068e53..1edf14adc 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -47,6 +47,7 @@ "dependencies": { "@pgflow/client": "workspace:*", "@pgflow/dsl": "workspace:*", + "@plausible-analytics/tracker": "^0.4.4", "@supabase/supabase-js": "^2.78.0", "@xyflow/svelte": "^1.4.1", "shiki": "^3.14.0" diff --git a/apps/demo/src/lib/analytics.ts b/apps/demo/src/lib/analytics.ts new file mode 100644 index 000000000..2d0b063e8 --- /dev/null +++ b/apps/demo/src/lib/analytics.ts @@ -0,0 +1,98 @@ +import { init, track as trackEvent } from '@plausible-analytics/tracker'; +import { browser } from '$app/environment'; + +interface PlausibleConfig { + domain: string; + apiHost?: string; + trackLocalhost?: boolean; +} + +let initialized = false; + +/** + * Initialize Plausible analytics tracking. + * Should be called once when the app loads. + * + * @param config - Configuration options for Plausible + * @param config.domain - Your site's domain (e.g., 'demo.pgflow.dev') + * @param config.apiHost - Optional custom API endpoint (for proxy setup) + * @param config.trackLocalhost - Whether to track events on localhost (default: false) + */ +export function initPlausible(config: PlausibleConfig): void { + // Only run in browser environment + if (!browser) { + return; + } + + // Prevent double initialization + if (initialized) { + console.warn('[Plausible] Already initialized'); + return; + } + + const { domain, apiHost, trackLocalhost = false } = config; + + try { + init({ + domain, + // If apiHost is provided, use it as the endpoint + ...(apiHost && { endpoint: `${apiHost}/event` }), + // Disable tracking on localhost unless explicitly enabled + captureOnLocalhost: trackLocalhost, + // Auto-capture pageviews + autoCapturePageviews: true, + // Track file downloads + fileDownloads: true, + // Track outbound links + outboundLinks: true + }); + + initialized = true; + console.log('[Plausible] Initialized successfully'); + } catch (error) { + console.error('[Plausible] Initialization failed:', error); + } +} + +/** + * Track a custom event in Plausible. + * + * @param eventName - Name of the event to track + * @param props - Optional properties to attach to the event + * @param revenue - Optional revenue tracking data + * + * @example + * ```ts + * // Simple event + * track('signup'); + * + * // Event with properties + * track('signup', { tier: 'pro', plan: 'monthly' }); + * + * // Event with revenue + * track('purchase', { product: 'pro-plan' }, { amount: 29.99, currency: 'USD' }); + * ``` + */ +export function track( + eventName: string, + props?: Record, + revenue?: { amount: number; currency: string } +): void { + if (!browser) { + return; + } + + if (!initialized) { + console.warn('[Plausible] Not initialized. Call initPlausible() first.'); + return; + } + + try { + trackEvent(eventName, { + ...(props && { props }), + ...(revenue && { revenue }) + }); + } catch (error) { + console.error(`[Plausible] Failed to track event "${eventName}":`, error); + } +} diff --git a/apps/demo/src/lib/components/CodePanel.svelte b/apps/demo/src/lib/components/CodePanel.svelte index 7efa8eac2..8d48d2438 100644 --- a/apps/demo/src/lib/components/CodePanel.svelte +++ b/apps/demo/src/lib/components/CodePanel.svelte @@ -16,7 +16,7 @@ let { flowState, selectedStep, hoveredStep }: Props = $props(); const dispatch = createEventDispatcher<{ - 'step-selected': { stepSlug: string | null }; + 'step-selected': { stepSlug: string | null; source?: string }; 'step-hovered': { stepSlug: string | null }; }>(); @@ -201,7 +201,7 @@ dispatch('step-hovered', { stepSlug: null }); // All sections (including flow_config) dispatch their slug - dispatch('step-selected', { stepSlug }); + dispatch('step-selected', { stepSlug, source: 'code' }); }; line.addEventListener('click', clickHandler); handlers.push({ element: line, type: 'click', handler: clickHandler }); diff --git a/apps/demo/src/lib/components/DAGVisualization.svelte b/apps/demo/src/lib/components/DAGVisualization.svelte index 28f8c58e2..df1a791c9 100644 --- a/apps/demo/src/lib/components/DAGVisualization.svelte +++ b/apps/demo/src/lib/components/DAGVisualization.svelte @@ -53,7 +53,7 @@ }); const dispatch = createEventDispatcher<{ - 'step-selected': { stepSlug: string }; + 'step-selected': { stepSlug: string; source?: string }; 'step-hovered': { stepSlug: string | null }; }>(); @@ -75,7 +75,7 @@ if (stepSlug) { // Clear hover state before navigating dispatch('step-hovered', { stepSlug: null }); - dispatch('step-selected', { stepSlug }); + dispatch('step-selected', { stepSlug, source: 'dag' }); } } diff --git a/apps/demo/src/lib/components/ExplanationPanel.svelte b/apps/demo/src/lib/components/ExplanationPanel.svelte index 2537f0325..272f9366c 100644 --- a/apps/demo/src/lib/components/ExplanationPanel.svelte +++ b/apps/demo/src/lib/components/ExplanationPanel.svelte @@ -358,7 +358,7 @@ {/if} -
+
{#if currentStepInfo} {#key selectedStep}
diff --git a/apps/demo/src/lib/components/WelcomeModal.svelte b/apps/demo/src/lib/components/WelcomeModal.svelte index dd0327647..869c9cb9d 100644 --- a/apps/demo/src/lib/components/WelcomeModal.svelte +++ b/apps/demo/src/lib/components/WelcomeModal.svelte @@ -4,12 +4,11 @@ interface Props { visible: boolean; - hasRun: boolean; onRunFlow: () => void; onDismiss: () => void; } - let { visible, hasRun, onRunFlow, onDismiss }: Props = $props(); + let { visible, onRunFlow, onDismiss }: Props = $props(); // Handle ESC key function handleKeyDown(event: KeyboardEvent) { @@ -31,100 +30,54 @@
- {#if !hasRun} -
- pgflow -

- Workflow orchestration that runs in your Supabase project -

-
- {:else} -

🎉 Workflow Complete

- {/if} +
+ pgflow +

+ Workflow orchestration that runs in your Supabase project +

+
- {#if !hasRun} - -
-

- This demo runs a 4-step DAG: fetch → (summarize - + extractKeywords in parallel) → - publish. -

- -
-

- Postgres handles orchestration, state management, output persistence, - retries, and queue management. -

-

- Auto-respawning Edge Function worker executes your handlers. -

-

- TypeScript Client wraps RPC and Realtime for starting flows and observing - state changes. -

-
- -
- - -
- -

- Click any step to inspect inputs, outputs, and dependencies +

+

+ This demo runs a 4-step DAG: fetch → (summarize + + extractKeywords in parallel) → + publish. +

+ +
+

+ Postgres handles orchestration, state management, output persistence, + retries, and queue management.

-
- {:else} - -
-

- What happened: +

+ Auto-respawning Edge Function worker executes your handlers.

- -
-

- start_flow() created step state in Postgres - and pushed messages to the queue. -

-

- Edge Function worker polled queue, executed handlers, and reported back. -

-

- SQL Core updated state, saved outputs, resolved dependencies, and scheduled - next steps. -

-

- Supabase Realtime broadcast each state change to update this UI in real-time. -

-
- -

- After each task completes, SQL Core finds steps with all dependencies met, pushes them - to the queue, and marks the run complete when no steps remain. +

+ TypeScript Client wraps RPC and Realtime for starting flows and observing + state changes.

+
-
- -
- -

- Click any step to see inputs, outputs, and execution data -

+
+ +
- {/if} + +

+ Click any step to inspect inputs, outputs, and dependencies +

+
diff --git a/apps/demo/src/routes/+layout.svelte b/apps/demo/src/routes/+layout.svelte index a50d72603..22407a301 100644 --- a/apps/demo/src/routes/+layout.svelte +++ b/apps/demo/src/routes/+layout.svelte @@ -1,8 +1,19 @@ diff --git a/apps/demo/src/routes/+page.svelte b/apps/demo/src/routes/+page.svelte index 80b95d6fd..b8184917f 100644 --- a/apps/demo/src/routes/+page.svelte +++ b/apps/demo/src/routes/+page.svelte @@ -15,6 +15,7 @@ import { Play, CheckCircle2, XCircle, Code, GitBranch, Radio, Loader2 } from '@lucide/svelte'; import { codeToHtml } from 'shiki'; import type ArticleFlow from '../../supabase/functions/article_flow_worker/article_flow'; + import { track } from '$lib/analytics'; const flowState = createFlowState(pgflow, 'article_flow', [ 'fetchArticle', @@ -35,6 +36,11 @@ let hasRunOnce = $state(false); let highlightButton = $state(false); + // Analytics tracking state + let flowStartTime: number | undefined; + let explanationOpenTime: number | undefined; + let eventsOpenTime: number | undefined; + // Show explanation panel when either a step is selected OR flow explanation is requested const explanationVisible = $derived(selectedStep !== null || showFlowExplanation); @@ -46,6 +52,16 @@ async function processArticle() { console.log('[DEBUG] processArticle called, selectedStep before:', selectedStep); + + // Track Flow Started + track('Flow Started', { + is_first_run: !hasRunOnce, + source: showWelcome ? 'welcome_modal' : 'header_button', + url_length: url.length + }); + + flowStartTime = Date.now(); + try { const run = await flowState.startFlow({ url }); console.log('Flow started:', run); @@ -53,6 +69,11 @@ console.log('[DEBUG] processArticle finished, selectedStep after:', selectedStep); } catch (error) { console.error('Failed to start flow:', error); + + // Track Flow Failed + track('Flow Failed', { + error_message: error instanceof Error ? error.message : String(error) + }); } } @@ -62,6 +83,15 @@ // Mark first run as complete (used for UI state tracking) hasRunOnce = true; // Don't show modal on completion - it breaks the flow of viewing results + + // Track Flow Completed + if (flowStartTime) { + const duration = Math.round((Date.now() - flowStartTime) / 1000); + track('Flow Completed', { + duration_seconds: duration, + total_steps: 4 + }); + } } }); @@ -81,27 +111,34 @@ const firstFailedSlug = failedSteps[0]; if (firstFailedSlug && selectedStep !== firstFailedSlug) { + const previousSelection = selectedStep; selectedStep = firstFailedSlug; showFlowExplanation = false; // Close mobile events panel if open (so explanation panel shows) mobileEventsVisible = false; mobileEventsContentVisible = false; + + // Track Failed Step Auto Selected + track('Failed Step Auto Selected', { + step_slug: firstFailedSlug, + previous_selection: previousSelection || 'none' + }); } } }); function handleRunFromModal() { - if (!hasRunOnce) { - // First time - close modal and run - showWelcome = false; - setTimeout(() => { - processArticle(); - }, 300); - } else { - // Running again - just close modal and run - showWelcome = false; + // Track Run Demo + track('Run Demo', { + source: 'welcome_modal', + is_first_run: !hasRunOnce + }); + + // Close modal and run + showWelcome = false; + setTimeout(() => { processArticle(); - } + }, 300); } function showPulseDots() { @@ -112,32 +149,58 @@ } function handleDismissModal() { + // Track Explore Code First + track('Explore Code First', { + dismissed_without_running: !hasRunOnce + }); + showWelcome = false; // Show pulsing dots on all clickable elements after any modal dismiss showPulseDots(); } - function handleStepSelected(event: CustomEvent<{ stepSlug: string | null }>) { + function handleStepSelected(event: CustomEvent<{ stepSlug: string | null; source?: string }>) { const clickedStep = event.detail.stepSlug; - console.log('Main page: handleStepSelected called with:', clickedStep); + const source = event.detail.source || 'unknown'; + console.log('Main page: handleStepSelected called with:', clickedStep, 'source:', source); if (clickedStep === null) { // Explicit clear selection request + const previousStep = selectedStep; selectedStep = null; showFlowExplanation = false; console.log('Main page: Cleared selection'); + + // Track Step Deselected + if (previousStep) { + track('Step Deselected', { + previous_step: previousStep, + method: 'clear_explicit' + }); + } } else if (clickedStep === 'flow_config') { // Clicking flow config: select it and show flow explanation if (selectedStep === 'flow_config') { // Toggle off selectedStep = null; showFlowExplanation = false; + + // Track Flow Config Deselected + track('Step Deselected', { + previous_step: 'flow_config', + method: 'toggle' + }); } else { selectedStep = 'flow_config'; showFlowExplanation = true; // Close events panel when opening explanation mobileEventsVisible = false; mobileEventsContentVisible = false; + + // Track Flow Config Selected + track('Flow Config Selected', { + source: 'code' + }); } console.log('Main page: Flow config toggled, showFlowExplanation:', showFlowExplanation); } else if (selectedStep === clickedStep) { @@ -145,14 +208,33 @@ selectedStep = null; showFlowExplanation = false; console.log('Main page: Toggled off - deselected step'); + + // Track Step Deselected + track('Step Deselected', { + previous_step: clickedStep, + method: 'toggle' + }); } else { // Select a different step + const previousStep = selectedStep; + const stepSlugs = ['fetchArticle', 'summarize', 'extractKeywords', 'publish']; + const stepIndex = stepSlugs.indexOf(clickedStep); + selectedStep = clickedStep; showFlowExplanation = false; // Close events panel when opening explanation mobileEventsVisible = false; mobileEventsContentVisible = false; console.log('Main page: Selected step:', selectedStep); + + // Track Step Selected + track('Step Selected', { + step_slug: clickedStep, + step_status: flowState.step(clickedStep).status || 'unknown', + source, + step_position: stepIndex, + previous_step: previousStep || 'none' + }); } } @@ -179,15 +261,62 @@ function closeExplanation() { // Closing explanation should clear both step selection and flow explanation + const previousStep = selectedStep; selectedStep = null; showFlowExplanation = false; + + // Track Explanation Closed + if (previousStep || showFlowExplanation) { + const isMobileDevice = typeof window !== 'undefined' && window.innerWidth < 768; + track('Explanation Closed', { + step_slug: previousStep || 'flow_config', + device: isMobileDevice ? 'mobile' : 'desktop', + time_open_seconds: explanationOpenTime + ? Math.round((Date.now() - explanationOpenTime) / 1000) + : 0 + }); + } } function clearSelection() { + const previousStep = selectedStep; selectedStep = null; showFlowExplanation = false; + + // Track Clear Selection Clicked + if (previousStep) { + track('Clear Selection Clicked', { + previous_step: previousStep, + device: 'desktop' + }); + } } + // Track explanation panel opening + $effect(() => { + if (explanationVisible) { + explanationOpenTime = Date.now(); + const isMobileDevice = typeof window !== 'undefined' && window.innerWidth < 768; + + // Determine trigger + let trigger: string; + if (showFlowExplanation) { + trigger = 'flow_config'; + } else { + // Check if this was auto-selected due to failure + const stepSlugs = ['fetchArticle', 'summarize', 'extractKeywords', 'publish']; + const failedSteps = stepSlugs.filter((slug) => flowState.step(slug).status === 'failed'); + trigger = failedSteps.includes(selectedStep || '') ? 'auto_failed_step' : 'step_selected'; + } + + track('Explanation Opened', { + trigger, + step_slug: selectedStep || 'flow_config', + device: isMobileDevice ? 'mobile' : 'desktop' + }); + } + }); + // Automatic cleanup on unmount onDestroy(() => flowState.dispose()); @@ -206,9 +335,18 @@ // Opening: show expanded content, then start height animation mobileEventsContentVisible = true; mobileEventsVisible = true; + eventsOpenTime = Date.now(); // Close explanation panel when opening events selectedStep = null; showFlowExplanation = false; + + // Track Events Panel Opened + const stepSlugs = ['fetchArticle', 'summarize', 'extractKeywords', 'publish']; + const failedSteps = stepSlugs.filter((slug) => flowState.step(slug).status === 'failed'); + track('Events Panel Opened', { + event_count: displayableEvents.length, + has_failed_events: failedSteps.length > 0 + }); } else { // Closing: start height animation, hide expanded content after animation finishes mobileEventsVisible = false; @@ -217,12 +355,28 @@ }, 300); // Match animation duration // Reset expanded event when closing panel expandedEventIdx = null; + + // Track Events Panel Closed + if (eventsOpenTime) { + track('Events Panel Closed', { + time_open_seconds: Math.round((Date.now() - eventsOpenTime) / 1000), + events_expanded: expandedEventIdx !== null + }); + } } } async function toggleEventExpanded(idx: number, event: unknown) { if (expandedEventIdx === idx) { expandedEventIdx = null; + + // Track Event Collapsed + const eventData = event as { event_type?: string; step_slug?: string }; + track('Event Collapsed', { + event_type: eventData.event_type || 'unknown', + step_slug: eventData.step_slug || 'unknown', + device: isMobile ? 'mobile' : 'desktop' + }); } else { // Generate syntax-highlighted JSON if not already cached if (!highlightedEventJson[idx]) { @@ -238,6 +392,15 @@ } // Set expanded after HTML is ready expandedEventIdx = idx; + + // Track Event Expanded + const eventData = event as { event_type?: string; step_slug?: string }; + track('Event Expanded', { + event_type: eventData.event_type || 'unknown', + step_slug: eventData.step_slug || 'unknown', + event_position: idx, + device: isMobile ? 'mobile' : 'desktop' + }); } } @@ -381,12 +544,7 @@ } - +
@@ -399,18 +557,27 @@ >
- + track('Logo Clicked', { location: 'header' })} + > pgflow pgflow | - Website + track('External Link Clicked', { destination: 'website', location: 'header' })}>Website | GitHub + track('External Link Clicked', { destination: 'github', location: 'header' })}>GitHub
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d54f035c4..71f7a1a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@pgflow/dsl': specifier: workspace:* version: link:../../pkgs/dsl + '@plausible-analytics/tracker': + specifier: ^0.4.4 + version: 0.4.4 '@supabase/supabase-js': specifier: ^2.78.0 version: 2.81.0 @@ -3855,6 +3858,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@plausible-analytics/tracker@0.4.4': + resolution: {integrity: sha512-fz0NOYUEYXtg1TBaPEEvtcBq3FfmLFuTe1VZw4M8sTWX129br5dguu3M15+plOQnc181ShYe67RfwhKgK89VnA==} + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -16789,6 +16795,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@plausible-analytics/tracker@0.4.4': {} + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2':