Creating A Google Analytics Droplet for the Marketplace Full Guide
This guide explains how to create a public droplet that allows clients to easily install Google Analytics tracking on their Fluid-powered websites. The droplet provides a simple interface for admins to input their Google Analytics ID and automatically creates or updates a Global Embed that injects the tracking script into all public theme pages.
Overview
A Google Analytics droplet consists of:
- Droplet Interface: A simple form where admins enter their Google Analytics ID
- API Integration: Backend logic that creates/updates Global Embeds via the Fluid API
- Global Embed Management: Automatic script injection into all public theme pages
Understanding Global Embeds
Global Embeds are a powerful Fluid feature that allows you to inject custom HTML content (typically JavaScript) into all public theme pages for a client. They work by:
How Global Embeds Work
- Script Placement: Global Embeds can be placed in either the
<head>or<body>section of pages - Automatic Injection: Once created and activated, the scripts are automatically injected into all public theme pages
- Company-Specific: Each company has their own set of Global Embeds
- Caching: Global Embeds are cached for performance and invalidated when updated
Global Embed Properties
- name: A descriptive name for the embed (e.g., "Google Analytics Tracking")
- content: The HTML/JavaScript content to inject
- status: Either "draft" or "active" (only active embeds are injected)
- placement: Either "head" or "body" (determines where the script is placed)
API Reference
Global Embeds API Endpoints
The Fluid API provides full CRUD operations for Global Embeds:
GET /api/global_embeds- List all Global EmbedsGET /api/global_embeds/:id- Get a specific Global EmbedPOST /api/global_embeds- Create a new Global EmbedPUT /api/global_embeds/:id- Update an existing Global EmbedDELETE /api/global_embeds/:id- Delete a Global Embed
Creating a Global Embed
curl -X POST "https://api.fluid.app/api/global_embeds" \ -H "Authorization: Bearer dit_xyz789..." \ -H "Content-Type: application/json" \ -d '{ "global_embed": { "name": "Google Analytics Tracking", "content": "<script async src=\"https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID\"></script><script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\"js\", new Date()); gtag(\"config\", \"GA_MEASUREMENT_ID\");</script>", "status": "active", "placement": "head" } }'
Updating a Global Embed
curl -X PUT "https://api.fluid.app/api/global_embeds/:id" \ -H "Authorization: Bearer dit_xyz789..." \ -H "Content-Type: application/json" \ -d '{ "global_embed": { "name": "Google Analytics Tracking", "content": "<script async src=\"https://www.googletagmanager.com/gtag/js?id=NEW_GA_ID\"></script><script>window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag(\"js\", new Date()); gtag(\"config\", \"NEW_GA_ID\");</script>", "status": "active" } }'
Droplet Implementation
1. Droplet Setup
First, create your droplet using the Fluid API:
curl -X POST "https://api.fluid.app/api/droplets" \ -H "Authorization: Bearer YOUR_OWNER_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "droplet": { "name": "Google Analytics Integration", "embed_url": "https://your-droplet.com/embed", "active": true, "settings": { "marketplace_page": { "title": "Google Analytics Integration", "summary": "Easily add Google Analytics tracking to your Fluid website", "logo_url": "https://your-droplet.com/ga-logo.svg" }, "details_page": { "title": "Google Analytics Integration", "summary": "Add Google Analytics tracking with just your measurement ID", "logo": "https://your-droplet.com/ga-logo-large.svg", "features": [ { "name": "Easy Setup", "summary": "Just enter your GA measurement ID", "details": "No technical knowledge required. Simply paste your Google Analytics measurement ID and we'll handle the rest." }, { "name": "Automatic Installation", "summary": "Scripts injected into all pages", "details": "Your tracking code is automatically added to all public pages of your Fluid website." } ] } } } }'
2. Webhook Registration
Register for installation webhooks to receive company authentication tokens:
curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_OWNER_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "resource": "droplet", "event": "installed", "url": "https://your-droplet.com/api/webhooks", "http_method": "post" } }'
3. Droplet Interface (HTML/JavaScript)
Create a simple interface for admins to input their Google Analytics ID:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Google Analytics Setup</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f8f9fa; } .container { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 8px; font-weight: 600; color: #333; } input[type="text"] { width: 100%; padding: 12px; border: 2px solid #e1e5e9; border-radius: 6px; font-size: 16px; transition: border-color 0.2s; } input[type="text"]:focus { outline: none; border-color: #007bff; } .help-text { font-size: 14px; color: #666; margin-top: 5px; } .btn { background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background-color 0.2s; } .btn:hover { background: #0056b3; } .btn:disabled { background: #6c757d; cursor: not-allowed; } .status { margin-top: 20px; padding: 12px; border-radius: 6px; display: none; } .status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .loading { display: none; margin-left: 10px; } .welcome-message { background: #e3f2fd; color: #1565c0; padding: 12px 16px; border-radius: 6px; margin-bottom: 20px; border-left: 4px solid #2196f3; font-weight: 500; } </style> </head> <body> <div class="container"> <div id="welcomeMessage" class="welcome-message" style="display: none;"> Welcome, <span id="companyName"></span>! </div> <h1>Google Analytics Setup</h1> <p>Enter your Google Analytics measurement ID to start tracking visitors on your Fluid website.</p> <form id="gaForm"> <div class="form-group"> <label for="gaId">Google Analytics Measurement ID</label> <input type="text" id="gaId" name="gaId" placeholder="G-XXXXXXXXXX" pattern="G-[A-Z0-9]{10}" required > <div class="help-text"> Find your measurement ID in your Google Analytics account under Admin > Data Streams </div> </div> <button type="submit" class="btn" id="submitBtn"> Save Google Analytics <span class="loading" id="loading">⏳</span> </button> </form> <div class="status" id="status"></div> </div> <script> // Extract company context from URL parameters const urlParams = new URLSearchParams(window.location.search); const dri = urlParams.get('dri'); if (!dri) { document.getElementById('status').textContent = 'Error: Missing company context. Please access this droplet through the Fluid interface.'; document.getElementById('status').className = 'status error'; document.getElementById('status').style.display = 'block'; document.getElementById('gaForm').style.display = 'none'; } else { // Load company information and existing configuration loadCompanyInfo(); loadExistingConfig(); } document.getElementById('gaForm').addEventListener('submit', async function(e) { e.preventDefault(); const gaId = document.getElementById('gaId').value.trim(); const submitBtn = document.getElementById('submitBtn'); const loading = document.getElementById('loading'); const status = document.getElementById('status'); if (!gaId.match(/^G-[A-Z0-9]{10}$/)) { status.textContent = 'Please enter a valid Google Analytics measurement ID (format: G-XXXXXXXXXX)'; status.className = 'status error'; status.style.display = 'block'; return; } submitBtn.disabled = true; loading.style.display = 'inline'; status.style.display = 'none'; try { await saveGoogleAnalytics(gaId); status.textContent = 'Google Analytics has been successfully configured! Your tracking code is now active on all pages.'; status.className = 'status success'; status.style.display = 'block'; } catch (error) { status.textContent = 'Error: ' + error.message; status.className = 'status error'; status.style.display = 'block'; } finally { submitBtn.disabled = false; loading.style.display = 'none'; } }); async function loadCompanyInfo() { try { const response = await fetch(`/api/companies/${dri}/info`); if (response.ok) { const data = await response.json(); if (data.companyName) { document.getElementById('companyName').textContent = data.companyName; document.getElementById('welcomeMessage').style.display = 'block'; } } } catch (error) { console.log('Could not load company information'); } } async function loadExistingConfig() { try { const response = await fetch(`/api/companies/${dri}/google-analytics`); if (response.ok) { const data = await response.json(); if (data.gaId) { document.getElementById('gaId').value = data.gaId; } } } catch (error) { console.log('No existing configuration found'); } } async function saveGoogleAnalytics(gaId) { const response = await fetch(`/api/companies/${dri}/google-analytics`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ gaId }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Failed to save Google Analytics configuration'); } } </script> </body> </html>
4. Backend Implementation (Node.js/Express Example)
const express = require('express'); const axios = require('axios'); const app = express(); app.use(express.json()); // Store company authentication tokens const companyTokens = new Map(); // Webhook endpoint for droplet installations app.post('/api/webhooks', async (req, res) => { const { resource, event, company } = req.body; if (resource === 'droplet' && event === 'installed') { // Store the company's authentication token companyTokens.set(company.droplet_uuid, { companyId: company.fluid_company_id, token: company.authentication_token, name: company.name }); console.log(`Company ${company.name} installed the droplet`); res.status(200).send('OK'); } else if (resource === 'droplet' && event === 'uninstalled') { // Remove company data companyTokens.delete(company.droplet_uuid); console.log(`Company ${company.name} uninstalled the droplet`); res.status(200).send('OK'); } else { res.status(400).send('Unknown webhook'); } }); // Get company information app.get('/api/companies/:dri/info', async (req, res) => { const { dri } = req.params; const companyData = companyTokens.get(dri); if (!companyData) { return res.status(404).json({ error: 'Company not found' }); } res.json({ companyName: companyData.name, companyId: companyData.companyId }); }); // Get existing Google Analytics configuration app.get('/api/companies/:dri/google-analytics', async (req, res) => { const { dri } = req.params; const companyData = companyTokens.get(dri); if (!companyData) { return res.status(404).json({ error: 'Company not found' }); } try { // Check if Google Analytics embed already exists const response = await axios.get('https://api.fluid.app/api/global_embeds', { headers: { 'Authorization': `Bearer ${companyData.token}`, 'Content-Type': 'application/json' } }); const gaEmbed = response.data.global_embeds.find(embed => embed.name === 'Google Analytics Tracking' ); if (gaEmbed) { // Extract GA ID from the script content const match = gaEmbed.content.match(/gtag\("config", "([^"]+)"/); res.json({ gaId: match ? match[1] : null }); } else { res.json({ gaId: null }); } } catch (error) { console.error('Error fetching GA config:', error); res.status(500).json({ error: 'Failed to fetch configuration' }); } }); // Save Google Analytics configuration app.post('/api/companies/:dri/google-analytics', async (req, res) => { const { dri } = req.params; const { gaId } = req.body; const companyData = companyTokens.get(dri); if (!companyData) { return res.status(404).json({ error: 'Company not found' }); } if (!gaId || !gaId.match(/^G-[A-Z0-9]{10}$/)) { return res.status(400).json({ error: 'Invalid Google Analytics ID' }); } try { // Check if Google Analytics embed already exists const listResponse = await axios.get('https://api.fluid.app/api/global_embeds', { headers: { 'Authorization': `Bearer ${companyData.token}`, 'Content-Type': 'application/json' } }); const existingEmbed = listResponse.data.global_embeds.find(embed => embed.name === 'Google Analytics Tracking' ); const gaScript = generateGoogleAnalyticsScript(gaId); if (existingEmbed) { // Update existing embed await axios.put(`https://api.fluid.app/api/global_embeds/${existingEmbed.id}`, { global_embed: { name: 'Google Analytics Tracking', content: gaScript, status: 'active', placement: 'head' } }, { headers: { 'Authorization': `Bearer ${companyData.token}`, 'Content-Type': 'application/json' } }); } else { // Create new embed await axios.post('https://api.fluid.app/api/global_embeds', { global_embed: { name: 'Google Analytics Tracking', content: gaScript, status: 'active', placement: 'head' } }, { headers: { 'Authorization': `Bearer ${companyData.token}`, 'Content-Type': 'application/json' } }); } res.json({ success: true }); } catch (error) { console.error('Error saving GA config:', error); res.status(500).json({ error: 'Failed to save Google Analytics configuration' }); } }); function generateGoogleAnalyticsScript(gaId) { return `<!-- Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=${gaId}"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', '${gaId}'); </script>`; } // Serve the droplet interface app.get('/embed', (req, res) => { res.sendFile(__dirname + '/public/embed.html'); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Droplet server running on port ${PORT}`); });
5. Package.json Dependencies
{ "name": "google-analytics-droplet", "version": "1.0.0", "description": "Google Analytics integration droplet for Fluid", "main": "server.js", "scripts": { "start": "node server.js", "dev": "nodemon server.js" }, "dependencies": { "express": "^4.18.2", "axios": "^1.6.0" }, "devDependencies": { "nodemon": "^3.0.1" } }
Testing Your Droplet
1. Local Development with ngrok
# Install ngrok npm install -g ngrok # Start your droplet server npm run dev # In another terminal, start ngrok ngrok http 3000 # Register webhook with ngrok URL curl -X POST "https://api.fluid.app/api/company/webhooks" \ -H "Authorization: Bearer YOUR_OWNER_COMPANY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhook": { "resource": "droplet", "event": "installed", "url": "https://abc123.ngrok.io/api/webhooks", "http_method": "post" } }'
2. Test Installation Flow
- Install your droplet through the Fluid interface
- Verify the webhook is received
- Test the embed interface with a valid GA measurement ID
- Check that the Global Embed is created in the company's account
- Visit a public page to verify the script is injected
Best Practices
Security
- Validate GA IDs: Ensure the measurement ID format is correct
- Sanitize Input: Clean any user input before using it in scripts
- Secure Tokens: Store authentication tokens securely and encrypted
Error Handling
- Graceful Degradation: Handle API failures gracefully
- User Feedback: Provide clear error messages to users
- Retry Logic: Implement retry with exponential backoff for API calls
Performance
- Cache Responses: Cache Global Embed data when possible
- Minimize API Calls: Batch operations when possible
- Async Operations: Use async/await for better performance
Monitoring
- Log Events: Log all important events (installations, updates, errors)
- Health Checks: Implement health check endpoints
- Metrics: Track usage and performance metrics
Troubleshooting
Common Issues
- Script Not Appearing: Check that the Global Embed status is "active"
- Wrong Placement: Verify the placement is set to "head" for GA scripts
- Authentication Errors: Ensure you're using the correct company token
- Cache Issues: Global Embeds are cached; changes may take a few minutes to appear
Debug Steps
- Check the company's Global Embeds via API:
curl -X GET "https://api.fluid.app/api/global_embeds" \ -H "Authorization: Bearer dit_xyz789..."
- Verify the script content is correct
- Check browser developer tools for any JavaScript errors
- Test with a simple script first before adding complex tracking
Conclusion
This guide provides everything needed to create a Google Analytics droplet for Fluid. The key concepts are:
- Global Embeds automatically inject scripts into all public theme pages
- Droplet Installation provides company-specific authentication tokens
- API Integration allows programmatic management of Global Embeds
- Simple Interface makes it easy for non-technical users to configure tracking
By following this guide, you can create a professional, user-friendly droplet that seamlessly integrates Google Analytics with Fluid websites.