Initial commit
This commit is contained in:
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