BREAKING CHANGE: Renamed from 13-gtm-audit to 13-ourdigital-gtm-manager New Features: - DataLayerInjector class for generating custom HTML tags - Support for 20+ GA4 event types (ecommerce, forms, video, engagement) - Subcommand structure: audit and inject modes - Preset tag generation (ecommerce, engagement, all) - DOM scraping option for dynamic value extraction - Generate tags from audit report findings - Korean payment method support (카카오페이, 네이버페이, 토스) CLI Changes: - `python gtm_manager.py audit --url ...` for auditing - `python gtm_manager.py inject --preset ecommerce` for tag generation - `python gtm_manager.py inject --event purchase --event add_to_cart` - `python gtm_manager.py inject --from-audit report.json` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
288 lines
6.3 KiB
Markdown
288 lines
6.3 KiB
Markdown
# DataLayer Validation Reference
|
|
|
|
## DataLayer Structure Basics
|
|
|
|
### Proper Initialization
|
|
```javascript
|
|
// Must appear BEFORE GTM script
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
</script>
|
|
<!-- GTM script here -->
|
|
```
|
|
|
|
### Push Syntax
|
|
```javascript
|
|
// Correct
|
|
dataLayer.push({ event: "page_view", page_title: "Home" });
|
|
|
|
// Wrong - direct assignment
|
|
dataLayer = [{ event: "page_view" }]; // ❌ Overwrites array
|
|
```
|
|
|
|
## Validation Rules
|
|
|
|
### Event Names
|
|
|
|
| Rule | Valid | Invalid |
|
|
|------|-------|---------|
|
|
| Alphanumeric + underscore | `add_to_cart` | `add-to-cart` |
|
|
| Max 40 characters | `purchase` | (too long names) |
|
|
| Case sensitive | `addToCart` ≠ `addtocart` | - |
|
|
| No spaces | `form_submit` | `form submit` |
|
|
| No special chars | `click_cta` | `click@cta` |
|
|
|
|
### Parameter Names
|
|
|
|
| Rule | Valid | Invalid |
|
|
|------|-------|---------|
|
|
| Max 40 characters | `item_category` | (too long) |
|
|
| Alphanumeric + underscore | `user_id` | `user-id` |
|
|
| Cannot start with `_` | `custom_param` | `_private` |
|
|
| Cannot start with number | `step_1` | `1_step` |
|
|
|
|
### Data Types
|
|
|
|
| Parameter | Expected Type | Example |
|
|
|-----------|---------------|---------|
|
|
| value | number | `29.99` not `"29.99"` |
|
|
| currency | string (ISO 4217) | `"USD"`, `"KRW"` |
|
|
| transaction_id | string | `"T_12345"` |
|
|
| quantity | integer | `2` not `2.0` |
|
|
| price | number | `45000` |
|
|
| items | array | `[{...}, {...}]` |
|
|
|
|
### Type Validation Code
|
|
|
|
```javascript
|
|
function validateDataLayerPush(data) {
|
|
const issues = [];
|
|
|
|
// Check value is number
|
|
if (data.ecommerce?.value !== undefined) {
|
|
if (typeof data.ecommerce.value !== 'number') {
|
|
issues.push(`value should be number, got ${typeof data.ecommerce.value}`);
|
|
}
|
|
}
|
|
|
|
// Check currency format
|
|
if (data.ecommerce?.currency) {
|
|
if (!/^[A-Z]{3}$/.test(data.ecommerce.currency)) {
|
|
issues.push(`currency should be 3-letter ISO code`);
|
|
}
|
|
}
|
|
|
|
// Check items array
|
|
if (data.ecommerce?.items) {
|
|
if (!Array.isArray(data.ecommerce.items)) {
|
|
issues.push(`items should be array`);
|
|
} else {
|
|
data.ecommerce.items.forEach((item, i) => {
|
|
if (!item.item_id) issues.push(`items[${i}] missing item_id`);
|
|
if (!item.item_name) issues.push(`items[${i}] missing item_name`);
|
|
if (item.price && typeof item.price !== 'number') {
|
|
issues.push(`items[${i}].price should be number`);
|
|
}
|
|
if (item.quantity && !Number.isInteger(item.quantity)) {
|
|
issues.push(`items[${i}].quantity should be integer`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
```
|
|
|
|
## E-commerce Object Clearing
|
|
|
|
### Why Clear?
|
|
GA4 may merge previous ecommerce data with new events.
|
|
|
|
### Correct Pattern
|
|
```javascript
|
|
// Clear first
|
|
dataLayer.push({ ecommerce: null });
|
|
|
|
// Then push new event
|
|
dataLayer.push({
|
|
event: "view_item",
|
|
ecommerce: { ... }
|
|
});
|
|
```
|
|
|
|
### Validation Check
|
|
```javascript
|
|
function checkEcommerceClear(dataLayerArray) {
|
|
let lastHadEcommerce = false;
|
|
const issues = [];
|
|
|
|
dataLayerArray.forEach((item, i) => {
|
|
const hasEcommerce = 'ecommerce' in item;
|
|
const isNull = item.ecommerce === null;
|
|
|
|
if (hasEcommerce && !isNull && lastHadEcommerce) {
|
|
issues.push({
|
|
index: i,
|
|
message: 'Missing ecommerce:null before this push'
|
|
});
|
|
}
|
|
|
|
lastHadEcommerce = hasEcommerce && !isNull;
|
|
});
|
|
|
|
return issues;
|
|
}
|
|
```
|
|
|
|
## Event Sequence Validation
|
|
|
|
### Expected Sequences
|
|
|
|
**E-commerce Purchase Flow:**
|
|
```
|
|
view_item_list? → view_item → add_to_cart → view_cart →
|
|
begin_checkout → add_shipping_info → add_payment_info → purchase
|
|
```
|
|
|
|
**Form Submission:**
|
|
```
|
|
form_start → form_submit → generate_lead?
|
|
```
|
|
|
|
**User Authentication:**
|
|
```
|
|
login | sign_up
|
|
```
|
|
|
|
### Sequence Validator
|
|
|
|
```javascript
|
|
function validateSequence(events, expectedOrder) {
|
|
const eventNames = events
|
|
.filter(e => e.event)
|
|
.map(e => e.event);
|
|
|
|
let lastIndex = -1;
|
|
const issues = [];
|
|
|
|
eventNames.forEach(event => {
|
|
const index = expectedOrder.indexOf(event);
|
|
if (index !== -1) {
|
|
if (index < lastIndex) {
|
|
issues.push(`${event} fired out of expected order`);
|
|
}
|
|
lastIndex = index;
|
|
}
|
|
});
|
|
|
|
return issues;
|
|
}
|
|
```
|
|
|
|
## Duplicate Event Detection
|
|
|
|
### Common Duplicates
|
|
- Multiple `page_view` on single page load
|
|
- `purchase` firing on page refresh
|
|
- Click events on bubbling elements
|
|
|
|
### Detection Code
|
|
|
|
```javascript
|
|
function findDuplicates(events) {
|
|
const seen = {};
|
|
const duplicates = [];
|
|
|
|
events.forEach((event, i) => {
|
|
if (!event.event) return;
|
|
|
|
const key = JSON.stringify(event);
|
|
if (seen[key]) {
|
|
duplicates.push({
|
|
event: event.event,
|
|
firstIndex: seen[key],
|
|
duplicateIndex: i
|
|
});
|
|
} else {
|
|
seen[key] = i;
|
|
}
|
|
});
|
|
|
|
return duplicates;
|
|
}
|
|
```
|
|
|
|
## Real-time Monitoring Setup
|
|
|
|
### Console Monitoring
|
|
|
|
```javascript
|
|
// Paste in browser console to monitor pushes
|
|
(function() {
|
|
const original = dataLayer.push;
|
|
dataLayer.push = function() {
|
|
console.group('📊 dataLayer.push');
|
|
console.log('Data:', arguments[0]);
|
|
console.log('Time:', new Date().toISOString());
|
|
console.groupEnd();
|
|
return original.apply(this, arguments);
|
|
};
|
|
console.log('✅ DataLayer monitoring active');
|
|
})();
|
|
```
|
|
|
|
### Export DataLayer
|
|
|
|
```javascript
|
|
// Copy full dataLayer to clipboard
|
|
copy(JSON.stringify(dataLayer, null, 2));
|
|
```
|
|
|
|
## Validation Checklist
|
|
|
|
### Structure
|
|
- [ ] dataLayer initialized before GTM
|
|
- [ ] Using push() not assignment
|
|
- [ ] Event names follow conventions
|
|
- [ ] Parameter names follow conventions
|
|
|
|
### Data Types
|
|
- [ ] value is number
|
|
- [ ] currency is 3-letter code
|
|
- [ ] quantity is integer
|
|
- [ ] items is array
|
|
- [ ] Required fields present
|
|
|
|
### E-commerce
|
|
- [ ] ecommerce:null before each push
|
|
- [ ] items array has item_id and item_name
|
|
- [ ] transaction_id is unique
|
|
- [ ] Consistent currency across events
|
|
|
|
### Sequence
|
|
- [ ] Events fire in logical order
|
|
- [ ] No duplicate events
|
|
- [ ] Purchase fires only once
|
|
|
|
## Debug Tools
|
|
|
|
### GTM Preview Mode
|
|
- Real-time event inspection
|
|
- Variable value checking
|
|
- Tag firing verification
|
|
|
|
### GA4 DebugView
|
|
- Live event stream
|
|
- Parameter validation
|
|
- User property tracking
|
|
|
|
### Browser Console
|
|
```javascript
|
|
// View current dataLayer
|
|
console.table(dataLayer);
|
|
|
|
// Filter by event
|
|
dataLayer.filter(d => d.event === 'purchase');
|
|
```
|