Initial commit
This commit is contained in:
408
skills/ga4-measurement-protocol/SKILL.md
Normal file
408
skills/ga4-measurement-protocol/SKILL.md
Normal file
@@ -0,0 +1,408 @@
|
||||
---
|
||||
name: ga4-measurement-protocol
|
||||
description: Complete guide to GA4 Measurement Protocol for server-side event tracking including API authentication, request format, validation, and implementation examples. Use when implementing server-side tracking, sending events from backend, working with Measurement Protocol API, integrating CRM with GA4, or tracking offline conversions. Covers API secrets, debug endpoint, Python/Node.js/PHP examples, and rate limits.
|
||||
---
|
||||
|
||||
# GA4 Measurement Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
The GA4 Measurement Protocol allows server-side event collection, enabling data transmission to GA4 from any HTTP-capable environment including backend servers, mobile apps, kiosks, and IoT devices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Invoke this skill when:
|
||||
|
||||
- Implementing server-side event tracking
|
||||
- Sending events from backend/server environments
|
||||
- Tracking offline conversions or transactions
|
||||
- Integrating CRM systems with GA4
|
||||
- Sending events from mobile app backends
|
||||
- Tracking server-to-server transactions
|
||||
- Implementing purchase tracking from payment processors
|
||||
- Tracking subscription renewals or recurring payments
|
||||
- Sending lead generation events from forms backend
|
||||
- Implementing custom server-side analytics
|
||||
- Working with headless CMS or API-first architectures
|
||||
- Debugging Measurement Protocol requests
|
||||
- Validating event payloads before production
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### API Endpoints
|
||||
|
||||
**Production Endpoint:**
|
||||
```
|
||||
POST https://www.google-analytics.com/mp/collect
|
||||
```
|
||||
|
||||
**Debug Endpoint (Validation):**
|
||||
```
|
||||
POST https://www.google-analytics.com/debug/mp/collect
|
||||
```
|
||||
|
||||
**Key Difference:** Debug endpoint returns validation messages without storing data.
|
||||
|
||||
### Authentication Requirements
|
||||
|
||||
**Two Credentials Required:**
|
||||
|
||||
1. **Measurement ID** (format: `G-XXXXXXXXXX`)
|
||||
- Find in: GA4 Admin → Data Streams → Web Stream details
|
||||
|
||||
2. **API Secret**
|
||||
- Generate in: Data Streams → Measurement Protocol API secrets → Create
|
||||
|
||||
**Generating API Secret:**
|
||||
1. GA4 Admin → Data Streams
|
||||
2. Click your data stream
|
||||
3. Scroll to "Measurement Protocol API secrets"
|
||||
4. Click "Create"
|
||||
5. Enter nickname (e.g., "Server-side tracking")
|
||||
6. Click "Create"
|
||||
7. **Copy secret immediately** (cannot retrieve later)
|
||||
|
||||
### Request Structure
|
||||
|
||||
**URL Format:**
|
||||
```
|
||||
https://www.google-analytics.com/mp/collect?measurement_id={MEASUREMENT_ID}&api_secret={API_SECRET}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Body (JSON):**
|
||||
```json
|
||||
{
|
||||
"client_id": "unique_client_identifier",
|
||||
"user_id": "optional_user_id",
|
||||
"timestamp_micros": "1234567890123456",
|
||||
"user_properties": {
|
||||
"property_name": {
|
||||
"value": "property_value"
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"ad_storage": "granted",
|
||||
"analytics_storage": "granted"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"name": "event_name",
|
||||
"params": {
|
||||
"parameter_name": "parameter_value",
|
||||
"value": 123.45,
|
||||
"currency": "USD"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
- **client_id**: Unique identifier for client (UUID recommended)
|
||||
- **events**: Array of event objects (max 25 events per request)
|
||||
- **events[].name**: Event name (string, ≤40 characters)
|
||||
|
||||
### Optional Fields
|
||||
|
||||
- **user_id**: User identifier for cross-device tracking
|
||||
- **timestamp_micros**: Event timestamp in microseconds (UTC)
|
||||
- **user_properties**: User-level properties
|
||||
- **consent**: Consent status (ad_storage, analytics_storage)
|
||||
|
||||
### Common Event Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `session_id` | string | Session identifier |
|
||||
| `engagement_time_msec` | integer | Engagement time in milliseconds |
|
||||
| `page_location` | string | Full URL |
|
||||
| `page_title` | string | Page title |
|
||||
| `value` | number | Monetary value |
|
||||
| `currency` | string | ISO 4217 currency code (USD, EUR) |
|
||||
| `transaction_id` | string | Unique transaction ID |
|
||||
| `items` | array | E-commerce items array |
|
||||
|
||||
### Python Implementation
|
||||
|
||||
**Using Requests Library:**
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
|
||||
MEASUREMENT_ID = "G-XXXXXXXXXX"
|
||||
API_SECRET = "your_api_secret"
|
||||
ENDPOINT = f"https://www.google-analytics.com/mp/collect?measurement_id={MEASUREMENT_ID}&api_secret={API_SECRET}"
|
||||
|
||||
def send_event(event_name, params=None):
|
||||
payload = {
|
||||
"client_id": str(uuid.uuid4()),
|
||||
"events": [{
|
||||
"name": event_name,
|
||||
"params": params or {}
|
||||
}]
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
ENDPOINT,
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(payload)
|
||||
)
|
||||
|
||||
return response.status_code == 204
|
||||
|
||||
# Send page view
|
||||
send_event("page_view", {
|
||||
"page_location": "https://example.com/page",
|
||||
"page_title": "Example Page"
|
||||
})
|
||||
|
||||
# Send purchase
|
||||
send_event("purchase", {
|
||||
"transaction_id": "T_12345",
|
||||
"value": 99.99,
|
||||
"currency": "USD",
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product Name",
|
||||
"price": 99.99,
|
||||
"quantity": 1
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
**Using ga4mp Library:**
|
||||
|
||||
```python
|
||||
# Install: pip install ga4mp
|
||||
from ga4mp import GtagMP
|
||||
|
||||
ga = GtagMP(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret",
|
||||
client_id="unique_client_id"
|
||||
)
|
||||
|
||||
# Send event
|
||||
ga.send_event(
|
||||
event_name="purchase",
|
||||
event_parameters={
|
||||
"transaction_id": "T_12345",
|
||||
"value": 99.99,
|
||||
"currency": "USD",
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product Name",
|
||||
"price": 99.99,
|
||||
"quantity": 1
|
||||
}]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Node.js Implementation
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
const MEASUREMENT_ID = 'G-XXXXXXXXXX';
|
||||
const API_SECRET = 'your_api_secret';
|
||||
const ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
|
||||
|
||||
async function sendEvent(eventName, params = {}) {
|
||||
const payload = {
|
||||
client_id: uuidv4(),
|
||||
events: [{
|
||||
name: eventName,
|
||||
params: params
|
||||
}]
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(ENDPOINT, payload, {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
return response.status === 204;
|
||||
} catch (error) {
|
||||
console.error('Error sending event:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Send purchase event
|
||||
sendEvent('purchase', {
|
||||
transaction_id: 'T_12345',
|
||||
value: 99.99,
|
||||
currency: 'USD',
|
||||
items: [{
|
||||
item_id: 'SKU_123',
|
||||
item_name: 'Product',
|
||||
price: 99.99,
|
||||
quantity: 1
|
||||
}]
|
||||
});
|
||||
```
|
||||
|
||||
### PHP Implementation
|
||||
|
||||
```php
|
||||
<?php
|
||||
// Using php-GA4-Measurement-Protocol library
|
||||
// Install: composer require br33f/php-ga4-measurement-protocol
|
||||
|
||||
use Br33f\Ga4\MeasurementProtocol\Dto\Event\PurchaseEvent;
|
||||
use Br33f\Ga4\MeasurementProtocol\Dto\Request\MeasurementRequest;
|
||||
use Br33f\Ga4\MeasurementProtocol\Service;
|
||||
|
||||
$measurementId = 'G-XXXXXXXXXX';
|
||||
$apiSecret = 'your_api_secret';
|
||||
|
||||
$service = new Service($apiSecret, $measurementId);
|
||||
|
||||
$event = new PurchaseEvent();
|
||||
$event->setTransactionId('T_12345')
|
||||
->setValue(99.99)
|
||||
->setCurrency('USD');
|
||||
|
||||
$request = new MeasurementRequest();
|
||||
$request->setClientId('unique_client_id')
|
||||
->addEvent($event);
|
||||
|
||||
$service->send($request);
|
||||
?>
|
||||
```
|
||||
|
||||
### Validation with Debug Endpoint
|
||||
|
||||
**Send to Debug Endpoint:**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"client_id": "test_client",
|
||||
"events": [{
|
||||
"name": "test_event",
|
||||
"params": {
|
||||
"test_param": "test_value"
|
||||
}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
|
||||
```json
|
||||
{
|
||||
"validationMessages": [
|
||||
{
|
||||
"fieldPath": "events[0].name",
|
||||
"description": "Event name must be 40 characters or fewer",
|
||||
"validationCode": "NAME_INVALID"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Empty Response = Valid:**
|
||||
- No validationMessages = payload valid
|
||||
- Status 200 = request processed
|
||||
|
||||
### Validation Codes
|
||||
|
||||
| Code | Description | Fix |
|
||||
|------|-------------|-----|
|
||||
| `NAME_INVALID` | Invalid event/parameter name | Use lowercase, underscores, ≤40 chars |
|
||||
| `NAME_RESERVED` | Reserved name used | Check GA4 reserved names list |
|
||||
| `VALUE_INVALID` | Invalid parameter value | Check data type, format |
|
||||
| `VALUE_REQUIRED` | Required value missing | Add required parameter |
|
||||
| `VALUE_OUT_OF_BOUNDS` | Value exceeds limits | Check numeric ranges |
|
||||
| `EXCEEDED_MAX_ENTITIES` | Too many events | Max 25 events per request |
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always Validate First:**
|
||||
- Use debug endpoint before production
|
||||
- Test with sample data
|
||||
- Verify response is empty (valid)
|
||||
|
||||
2. **Use Consistent client_id:**
|
||||
- Same user = same client_id across sessions
|
||||
- Store in database for logged-in users
|
||||
- Use UUID format for anonymity
|
||||
|
||||
3. **Include session_id:**
|
||||
- Maintain session continuity
|
||||
- Generate unique session ID
|
||||
- Keep consistent within session
|
||||
|
||||
4. **Batch Events:**
|
||||
- Send up to 25 events per request
|
||||
- More efficient than individual requests
|
||||
- Reduces API calls
|
||||
|
||||
5. **Handle Errors Gracefully:**
|
||||
- Implement retry logic with exponential backoff
|
||||
- Log failed requests
|
||||
- Queue events for retry
|
||||
|
||||
6. **Set Proper Timestamps:**
|
||||
- Use `timestamp_micros` for historical data
|
||||
- Convert to microseconds: `timestamp_ms * 1000`
|
||||
- Max 3 days in past, 72 hours in future
|
||||
|
||||
7. **Respect Consent:**
|
||||
- Set consent parameters appropriately
|
||||
- Match frontend consent status
|
||||
- Required for GDPR compliance
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- **ga4-setup** - Initial GA4 property and data stream setup
|
||||
- **ga4-debugview** - Testing Measurement Protocol events in DebugView
|
||||
- **ga4-recommended-events** - Sending recommended event structures
|
||||
- **ga4-custom-events** - Server-side custom event implementation
|
||||
- **ga4-user-tracking** - Implementing User ID server-side
|
||||
- **ga4-privacy-compliance** - Setting consent parameters correctly
|
||||
- **ga4-bigquery** - Analyzing server-side events in BigQuery
|
||||
|
||||
## References
|
||||
|
||||
- **references/measurement-protocol-complete.md** - Full API reference and examples
|
||||
- **references/authentication-setup.md** - API secrets and authentication guide
|
||||
- **references/event-validation.md** - Validation patterns and troubleshooting
|
||||
- **references/implementation-examples.md** - Real-world implementation patterns (Python, Node.js, PHP)
|
||||
- **references/rate-limits-best-practices.md** - Rate limits, batching, and optimization
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Generate API Secret:**
|
||||
Admin → Data Streams → Measurement Protocol API secrets → Create
|
||||
|
||||
**Endpoint:**
|
||||
- Production: `/mp/collect`
|
||||
- Debug: `/debug/mp/collect`
|
||||
|
||||
**Required Fields:**
|
||||
- client_id (UUID recommended)
|
||||
- events array
|
||||
- event name
|
||||
|
||||
**Max Limits:**
|
||||
- 25 events per request
|
||||
- 40 characters per event name
|
||||
- 25 event parameters per event
|
||||
- 25 user properties per request
|
||||
|
||||
**Validation:**
|
||||
- Send to debug endpoint
|
||||
- Empty response = valid
|
||||
- Check validationMessages array
|
||||
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
GA4 Measurement Protocol - Complete Python Implementation Example
|
||||
Demonstrates server-side event tracking with validation, error handling, and batching
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
class GA4MeasurementProtocol:
|
||||
"""GA4 Measurement Protocol client with validation and error handling"""
|
||||
|
||||
def __init__(self, measurement_id: str, api_secret: str, debug: bool = False):
|
||||
"""
|
||||
Initialize GA4 Measurement Protocol client
|
||||
|
||||
Args:
|
||||
measurement_id: GA4 Measurement ID (G-XXXXXXXXXX)
|
||||
api_secret: API secret from GA4 Data Streams
|
||||
debug: Use debug endpoint for validation
|
||||
"""
|
||||
self.measurement_id = measurement_id
|
||||
self.api_secret = api_secret
|
||||
self.debug = debug
|
||||
|
||||
self.endpoint = self._get_endpoint()
|
||||
|
||||
def _get_endpoint(self) -> str:
|
||||
"""Get appropriate endpoint (production or debug)"""
|
||||
base = "debug/mp" if self.debug else "mp"
|
||||
return f"https://www.google-analytics.com/{base}/collect?measurement_id={self.measurement_id}&api_secret={self.api_secret}"
|
||||
|
||||
def send_event(
|
||||
self,
|
||||
client_id: str,
|
||||
event_name: str,
|
||||
event_params: Optional[Dict] = None,
|
||||
user_id: Optional[str] = None,
|
||||
user_properties: Optional[Dict] = None,
|
||||
timestamp_micros: Optional[int] = None,
|
||||
consent: Optional[Dict] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send single event to GA4
|
||||
|
||||
Args:
|
||||
client_id: Unique client identifier (UUID)
|
||||
event_name: Event name (lowercase, underscores, ≤40 chars)
|
||||
event_params: Event parameters dictionary
|
||||
user_id: Optional user ID for cross-device tracking
|
||||
user_properties: Optional user properties
|
||||
timestamp_micros: Optional event timestamp in microseconds
|
||||
consent: Optional consent status
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
payload = self._build_payload(
|
||||
client_id=client_id,
|
||||
events=[{
|
||||
"name": event_name,
|
||||
"params": event_params or {}
|
||||
}],
|
||||
user_id=user_id,
|
||||
user_properties=user_properties,
|
||||
timestamp_micros=timestamp_micros,
|
||||
consent=consent
|
||||
)
|
||||
|
||||
return self._send_request(payload)
|
||||
|
||||
def send_batch(
|
||||
self,
|
||||
client_id: str,
|
||||
events: List[Dict],
|
||||
user_id: Optional[str] = None,
|
||||
user_properties: Optional[Dict] = None,
|
||||
consent: Optional[Dict] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send batch of events (max 25)
|
||||
|
||||
Args:
|
||||
client_id: Unique client identifier
|
||||
events: List of event dictionaries with 'name' and 'params'
|
||||
user_id: Optional user ID
|
||||
user_properties: Optional user properties
|
||||
consent: Optional consent status
|
||||
|
||||
Returns:
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
if len(events) > 25:
|
||||
raise ValueError("Maximum 25 events per batch")
|
||||
|
||||
payload = self._build_payload(
|
||||
client_id=client_id,
|
||||
events=events,
|
||||
user_id=user_id,
|
||||
user_properties=user_properties,
|
||||
consent=consent
|
||||
)
|
||||
|
||||
return self._send_request(payload)
|
||||
|
||||
def _build_payload(
|
||||
self,
|
||||
client_id: str,
|
||||
events: List[Dict],
|
||||
user_id: Optional[str] = None,
|
||||
user_properties: Optional[Dict] = None,
|
||||
timestamp_micros: Optional[int] = None,
|
||||
consent: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Build request payload"""
|
||||
payload = {
|
||||
"client_id": client_id,
|
||||
"events": events
|
||||
}
|
||||
|
||||
if user_id:
|
||||
payload["user_id"] = user_id
|
||||
|
||||
if user_properties:
|
||||
# Format user properties
|
||||
formatted_props = {
|
||||
key: {"value": value}
|
||||
for key, value in user_properties.items()
|
||||
}
|
||||
payload["user_properties"] = formatted_props
|
||||
|
||||
if timestamp_micros:
|
||||
payload["timestamp_micros"] = str(timestamp_micros)
|
||||
|
||||
if consent:
|
||||
payload["consent"] = consent
|
||||
|
||||
return payload
|
||||
|
||||
def _send_request(self, payload: Dict, max_retries: int = 3) -> bool:
|
||||
"""
|
||||
Send request with retry logic
|
||||
|
||||
Args:
|
||||
payload: Request payload
|
||||
max_retries: Maximum retry attempts
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.post(
|
||||
self.endpoint,
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(payload),
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 204:
|
||||
return True
|
||||
|
||||
if response.status_code == 200 and self.debug:
|
||||
# Debug endpoint returns validation messages
|
||||
validation = response.json()
|
||||
if not validation.get("validationMessages"):
|
||||
print("✓ Validation passed")
|
||||
return True
|
||||
else:
|
||||
print("✗ Validation errors:")
|
||||
for msg in validation["validationMessages"]:
|
||||
print(f" - {msg['fieldPath']}: {msg['description']}")
|
||||
return False
|
||||
|
||||
if response.status_code == 429:
|
||||
# Rate limited - exponential backoff
|
||||
wait_time = (2 ** attempt) + (time.time() % 1)
|
||||
print(f"Rate limited. Waiting {wait_time:.2f}s...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
|
||||
print(f"Error: HTTP {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# USAGE EXAMPLES
|
||||
# ============================================================================
|
||||
|
||||
def example_page_view():
|
||||
"""Example: Track page view"""
|
||||
ga = GA4MeasurementProtocol(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret",
|
||||
debug=True # Use debug endpoint for testing
|
||||
)
|
||||
|
||||
success = ga.send_event(
|
||||
client_id=str(uuid.uuid4()),
|
||||
event_name="page_view",
|
||||
event_params={
|
||||
"page_location": "https://example.com/products",
|
||||
"page_title": "Products Page",
|
||||
"page_referrer": "https://google.com"
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Page view sent: {success}")
|
||||
|
||||
|
||||
def example_purchase():
|
||||
"""Example: Track purchase"""
|
||||
ga = GA4MeasurementProtocol(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret"
|
||||
)
|
||||
|
||||
success = ga.send_event(
|
||||
client_id="client_12345",
|
||||
event_name="purchase",
|
||||
event_params={
|
||||
"transaction_id": f"T_{int(time.time())}",
|
||||
"value": 99.99,
|
||||
"currency": "USD",
|
||||
"tax": 8.00,
|
||||
"shipping": 5.00,
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Blue T-Shirt",
|
||||
"price": 99.99,
|
||||
"quantity": 1,
|
||||
"item_category": "Apparel"
|
||||
}]
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Purchase sent: {success}")
|
||||
|
||||
|
||||
def example_user_signup():
|
||||
"""Example: Track user signup with user properties"""
|
||||
ga = GA4MeasurementProtocol(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret"
|
||||
)
|
||||
|
||||
success = ga.send_event(
|
||||
client_id=str(uuid.uuid4()),
|
||||
user_id="user_456",
|
||||
event_name="sign_up",
|
||||
event_params={
|
||||
"method": "email"
|
||||
},
|
||||
user_properties={
|
||||
"user_tier": "free",
|
||||
"signup_date": datetime.now().strftime("%Y-%m-%d")
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Sign up sent: {success}")
|
||||
|
||||
|
||||
def example_batch_events():
|
||||
"""Example: Send multiple events in batch"""
|
||||
ga = GA4MeasurementProtocol(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret"
|
||||
)
|
||||
|
||||
events = [
|
||||
{
|
||||
"name": "view_item",
|
||||
"params": {
|
||||
"currency": "USD",
|
||||
"value": 29.99,
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product",
|
||||
"price": 29.99
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "add_to_cart",
|
||||
"params": {
|
||||
"currency": "USD",
|
||||
"value": 29.99,
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product",
|
||||
"price": 29.99,
|
||||
"quantity": 1
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "begin_checkout",
|
||||
"params": {
|
||||
"currency": "USD",
|
||||
"value": 34.99,
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product",
|
||||
"price": 29.99,
|
||||
"quantity": 1
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
success = ga.send_batch(
|
||||
client_id="client_789",
|
||||
events=events
|
||||
)
|
||||
|
||||
print(f"Batch sent: {success}")
|
||||
|
||||
|
||||
def example_with_consent():
|
||||
"""Example: Send event with consent parameters"""
|
||||
ga = GA4MeasurementProtocol(
|
||||
measurement_id="G-XXXXXXXXXX",
|
||||
api_secret="your_api_secret"
|
||||
)
|
||||
|
||||
success = ga.send_event(
|
||||
client_id=str(uuid.uuid4()),
|
||||
event_name="page_view",
|
||||
event_params={
|
||||
"page_location": "https://example.com"
|
||||
},
|
||||
consent={
|
||||
"ad_storage": "denied",
|
||||
"analytics_storage": "granted",
|
||||
"ad_user_data": "denied",
|
||||
"ad_personalization": "denied"
|
||||
}
|
||||
)
|
||||
|
||||
print(f"Event with consent sent: {success}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run examples
|
||||
print("=== GA4 Measurement Protocol Examples ===\n")
|
||||
|
||||
print("1. Page View:")
|
||||
example_page_view()
|
||||
|
||||
print("\n2. Purchase:")
|
||||
example_purchase()
|
||||
|
||||
print("\n3. User Signup:")
|
||||
example_user_signup()
|
||||
|
||||
print("\n4. Batch Events:")
|
||||
example_batch_events()
|
||||
|
||||
print("\n5. With Consent:")
|
||||
example_with_consent()
|
||||
479
skills/ga4-measurement-protocol/references/complete-api-guide.md
Normal file
479
skills/ga4-measurement-protocol/references/complete-api-guide.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# GA4 Measurement Protocol Complete API Guide
|
||||
|
||||
## API Overview
|
||||
|
||||
**Base URL:** `https://www.google-analytics.com/mp/collect`
|
||||
**Debug URL:** `https://www.google-analytics.com/debug/mp/collect`
|
||||
|
||||
**Method:** POST
|
||||
**Content-Type:** application/json
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Secret Creation
|
||||
|
||||
1. **GA4 Admin** → **Data Streams**
|
||||
2. Select your data stream
|
||||
3. **Measurement Protocol API secrets** section
|
||||
4. Click **Create**
|
||||
5. Name: "Server tracking" (or descriptive name)
|
||||
6. Click **Create**
|
||||
7. **Copy secret value** (shown only once!)
|
||||
|
||||
**Storage:**
|
||||
- Store secret securely (environment variables, secrets manager)
|
||||
- Never commit to version control
|
||||
- Regenerate if exposed
|
||||
|
||||
### Measurement ID
|
||||
|
||||
**Format:** `G-XXXXXXXXXX`
|
||||
|
||||
**Find in:**
|
||||
- GA4 Admin → Data Streams → Web Stream
|
||||
- "Measurement ID" field at top
|
||||
|
||||
## Request Format
|
||||
|
||||
### Complete URL
|
||||
|
||||
```
|
||||
POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
- `measurement_id`: GA4 Measurement ID
|
||||
- `api_secret`: API secret from GA4
|
||||
|
||||
### Request Body Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "CLIENT_ID_HERE",
|
||||
"user_id": "USER_123",
|
||||
"timestamp_micros": "1234567890123456",
|
||||
"user_properties": {
|
||||
"user_tier": {
|
||||
"value": "premium"
|
||||
},
|
||||
"lifetime_value": {
|
||||
"value": 1250.50
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"ad_storage": "granted",
|
||||
"analytics_storage": "granted",
|
||||
"ad_user_data": "granted",
|
||||
"ad_personalization": "granted"
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"name": "purchase",
|
||||
"params": {
|
||||
"transaction_id": "T_12345",
|
||||
"value": 99.99,
|
||||
"currency": "USD",
|
||||
"tax": 8.00,
|
||||
"shipping": 5.00,
|
||||
"items": [
|
||||
{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Product Name",
|
||||
"price": 99.99,
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Field Specifications
|
||||
|
||||
### client_id (Required)
|
||||
|
||||
**Type:** String
|
||||
**Format:** UUID recommended
|
||||
**Purpose:** Unique client identifier
|
||||
|
||||
**Recommendations:**
|
||||
- Use UUID v4 format
|
||||
- Persist for same user across sessions
|
||||
- Store in database for logged-in users
|
||||
- Never use PII (email, name, etc.)
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
import uuid
|
||||
client_id = str(uuid.uuid4())
|
||||
```
|
||||
|
||||
### user_id (Optional)
|
||||
|
||||
**Type:** String
|
||||
**Purpose:** Cross-device tracking
|
||||
|
||||
**When to Use:**
|
||||
- User is logged in
|
||||
- Want to track across devices
|
||||
- Have user identifier
|
||||
|
||||
**Important:**
|
||||
- Don't use email or PII directly
|
||||
- Hash or use internal ID
|
||||
- Must also send client_id
|
||||
|
||||
### timestamp_micros (Optional)
|
||||
|
||||
**Type:** String (integer as string)
|
||||
**Format:** Microseconds since Unix epoch
|
||||
|
||||
**Purpose:**
|
||||
- Send historical events
|
||||
- Backfill data
|
||||
- Correct timing for delayed events
|
||||
|
||||
**Limits:**
|
||||
- Max 3 days in past
|
||||
- Max 72 hours in future
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
import time
|
||||
timestamp_micros = str(int(time.time() * 1_000_000))
|
||||
```
|
||||
|
||||
### user_properties (Optional)
|
||||
|
||||
**Type:** Object
|
||||
**Purpose:** Set user-level properties
|
||||
|
||||
**Format:**
|
||||
```json
|
||||
{
|
||||
"property_name": {
|
||||
"value": "property_value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Limits:**
|
||||
- Max 25 user properties per request
|
||||
- Property name ≤24 characters
|
||||
- Value ≤36 characters
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"user_tier": {"value": "premium"},
|
||||
"signup_date": {"value": "2024-01-15"},
|
||||
"total_purchases": {"value": 12}
|
||||
}
|
||||
```
|
||||
|
||||
### consent (Optional)
|
||||
|
||||
**Type:** Object
|
||||
**Purpose:** Set consent status
|
||||
|
||||
**Fields:**
|
||||
- `ad_storage`: "granted" | "denied"
|
||||
- `analytics_storage`: "granted" | "denied"
|
||||
- `ad_user_data`: "granted" | "denied"
|
||||
- `ad_personalization`: "granted" | "denied"
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"ad_storage": "denied",
|
||||
"analytics_storage": "granted"
|
||||
}
|
||||
```
|
||||
|
||||
### events (Required)
|
||||
|
||||
**Type:** Array
|
||||
**Limits:** Max 25 events per request
|
||||
|
||||
**Event Structure:**
|
||||
```json
|
||||
{
|
||||
"name": "event_name",
|
||||
"params": {
|
||||
"parameter": "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Event Name Rules:**
|
||||
- Lowercase letters, numbers, underscores
|
||||
- Max 40 characters
|
||||
- Cannot start with number
|
||||
- Avoid reserved names
|
||||
|
||||
**Event Parameters:**
|
||||
- Max 25 parameters per event
|
||||
- Parameter name ≤40 characters
|
||||
- String value ≤100 characters
|
||||
|
||||
## Common Event Examples
|
||||
|
||||
### Page View
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "client_123",
|
||||
"events": [{
|
||||
"name": "page_view",
|
||||
"params": {
|
||||
"page_location": "https://example.com/products",
|
||||
"page_title": "Products Page",
|
||||
"page_referrer": "https://google.com"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Purchase
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "client_123",
|
||||
"events": [{
|
||||
"name": "purchase",
|
||||
"params": {
|
||||
"transaction_id": "T_12345",
|
||||
"value": 99.99,
|
||||
"currency": "USD",
|
||||
"tax": 8.00,
|
||||
"shipping": 5.00,
|
||||
"items": [{
|
||||
"item_id": "SKU_123",
|
||||
"item_name": "Blue T-Shirt",
|
||||
"price": 99.99,
|
||||
"quantity": 1,
|
||||
"item_category": "Apparel"
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Lead Generation
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "client_123",
|
||||
"events": [{
|
||||
"name": "generate_lead",
|
||||
"params": {
|
||||
"currency": "USD",
|
||||
"value": 50.00,
|
||||
"form_name": "contact_form",
|
||||
"lead_source": "website"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Sign Up
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "client_123",
|
||||
"user_id": "user_456",
|
||||
"events": [{
|
||||
"name": "sign_up",
|
||||
"params": {
|
||||
"method": "email"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Event
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "client_123",
|
||||
"events": [{
|
||||
"name": "subscription_renewed",
|
||||
"params": {
|
||||
"subscription_tier": "premium",
|
||||
"renewal_amount": 29.99,
|
||||
"currency": "USD",
|
||||
"subscription_id": "SUB_789"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Endpoint Validation
|
||||
|
||||
### Using cURL
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
"https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"client_id": "test_123",
|
||||
"events": [{
|
||||
"name": "purchase",
|
||||
"params": {
|
||||
"transaction_id": "T_TEST",
|
||||
"value": 99.99,
|
||||
"currency": "USD"
|
||||
}
|
||||
}]
|
||||
}'
|
||||
```
|
||||
|
||||
### Valid Response
|
||||
|
||||
**Status:** 200 OK
|
||||
|
||||
**Body (Valid):**
|
||||
```json
|
||||
{
|
||||
"validationMessages": []
|
||||
}
|
||||
```
|
||||
|
||||
**Empty array = No validation errors**
|
||||
|
||||
### Invalid Response
|
||||
|
||||
**Body (Invalid):**
|
||||
```json
|
||||
{
|
||||
"validationMessages": [
|
||||
{
|
||||
"fieldPath": "events[0].name",
|
||||
"description": "Event name must be 40 characters or fewer.",
|
||||
"validationCode": "NAME_INVALID"
|
||||
},
|
||||
{
|
||||
"fieldPath": "events[0].params.value",
|
||||
"description": "Event parameter value is invalid.",
|
||||
"validationCode": "VALUE_INVALID"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limits and Quotas
|
||||
|
||||
**No Official Published Limits**
|
||||
|
||||
**Recommendations:**
|
||||
- Batch events when possible (max 25/request)
|
||||
- Implement exponential backoff for retries
|
||||
- Monitor for rate limit responses
|
||||
- Don't exceed ~1 request per second per client
|
||||
|
||||
**Best Practices:**
|
||||
- Queue events on server
|
||||
- Batch send periodically
|
||||
- Retry failed requests with backoff
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Python Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
import time
|
||||
|
||||
def send_with_retry(payload, max_retries=3):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.post(ENDPOINT, json=payload)
|
||||
|
||||
if response.status_code == 204:
|
||||
return True # Success
|
||||
|
||||
if response.status_code == 429: # Rate limited
|
||||
wait = (2 ** attempt) + random.random()
|
||||
time.sleep(wait)
|
||||
continue
|
||||
|
||||
# Other error
|
||||
print(f"Error: {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
print(f"Request failed: {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### Node.js Example
|
||||
|
||||
```javascript
|
||||
async function sendWithRetry(payload, maxRetries = 3) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await axios.post(ENDPOINT, payload);
|
||||
|
||||
if (response.status === 204) {
|
||||
return true; // Success
|
||||
}
|
||||
|
||||
if (response.status === 429) { // Rate limited
|
||||
const wait = Math.pow(2, attempt) * 1000;
|
||||
await new Promise(resolve => setTimeout(resolve, wait));
|
||||
continue;
|
||||
}
|
||||
|
||||
console.error('Error:', response.status);
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error.message);
|
||||
if (attempt < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] API secret generated and stored securely
|
||||
- [ ] Measurement ID correct format (G-XXXXXXXXXX)
|
||||
- [ ] client_id generated (UUID format)
|
||||
- [ ] Events validated using debug endpoint
|
||||
- [ ] No validation errors in debug response
|
||||
- [ ] Events appear in GA4 DebugView (with debug_mode=true)
|
||||
- [ ] Parameters have correct data types
|
||||
- [ ] No PII in parameters
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Retry logic with exponential backoff
|
||||
- [ ] Events batched when appropriate
|
||||
- [ ] Consent parameters set correctly
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Events Not Appearing:**
|
||||
- Wrong Measurement ID
|
||||
- Wrong API secret
|
||||
- Events not validated first
|
||||
- Network/firewall blocking requests
|
||||
|
||||
**Validation Errors:**
|
||||
- Event name too long (>40 characters)
|
||||
- Reserved event name used
|
||||
- Invalid parameter values
|
||||
- Wrong data types
|
||||
|
||||
**Missing Data:**
|
||||
- client_id not consistent
|
||||
- timestamp_micros out of range
|
||||
- Parameters missing or misspelled
|
||||
Reference in New Issue
Block a user