diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-audit/CLAUDE.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/CLAUDE.md new file mode 100644 index 0000000..6e0bb46 --- /dev/null +++ b/ourdigital-custom-skills/13-ourdigital-gtm-audit/CLAUDE.md @@ -0,0 +1,109 @@ +# OurDigital GTM Audit + +Lightweight Google Tag Manager audit toolkit using Playwright browser automation. + +> For comprehensive GTM management including dataLayer tag generation, see `14-ourdigital-gtm-manager`. + +## Project Overview + +This tool audits GTM container installations, validates dataLayer events, tests form tracking, simulates e-commerce checkout flows, and generates comprehensive reports. + +## Quick Commands + +```bash +# Install dependencies +pip install playwright +playwright install chromium + +# Run full audit +python gtm_audit.py --url "https://example.com" --journey full + +# Form tracking audit +python gtm_audit.py --url "https://example.com/contact" --journey form + +# E-commerce checkout flow +python gtm_audit.py --url "https://example.com/cart" --journey checkout + +# DataLayer deep inspection +python gtm_audit.py --url "https://example.com" --journey datalayer + +# With specific container validation +python gtm_audit.py --url "https://example.com" --container "GTM-XXXXXX" +``` + +## Journey Types + +| Journey | Description | +|---------|-------------| +| `pageview` | Basic page load + scroll simulation | +| `scroll` | Scroll depth trigger testing (25%, 50%, 75%, 90%) | +| `form` | Form discovery, field analysis, interaction simulation | +| `checkout` | E-commerce flow: cart → checkout → shipping → payment → purchase | +| `datalayer` | Deep dataLayer validation and event sequence analysis | +| `full` | All of the above combined | + +## Output + +Generates `gtm_audit_report.json` with: +- Container status (installed, position, duplicates) +- DataLayer analysis (events, validation issues, sequence errors) +- Form analysis (forms found, tracking readiness, missing events) +- Checkout analysis (elements detected, flow issues) +- Network requests (GA4, Meta, LinkedIn, etc.) +- Recommendations and checklist + +## Key Files + +- `gtm_audit.py` - Main audit script +- `docs/ga4_events.md` - GA4 event specifications +- `docs/ecommerce_schema.md` - E-commerce dataLayer structures +- `docs/form_tracking.md` - Form event patterns +- `docs/checkout_flow.md` - Checkout funnel sequence +- `docs/datalayer_validation.md` - Validation rules +- `docs/common_issues.md` - Frequent problems and fixes + +## Coding Guidelines + +When modifying this tool: + +1. **Tag Destinations**: Add new platforms to `TAG_DESTINATIONS` dict +2. **Event Validation**: Add requirements to `GA4_EVENT_REQUIREMENTS` dict +3. **Form Selectors**: Extend `FormAnalyzer.discover_forms()` for custom forms +4. **Checkout Elements**: Add selectors to `CheckoutFlowAnalyzer.detect_checkout_elements()` + +## Korean Market Considerations + +- Support Korean payment methods (카카오페이, 네이버페이, 토스) +- Handle KRW currency (no decimals) +- Include Kakao Pixel and Naver Analytics patterns +- Korean button text patterns (장바구니, 결제하기, 주문하기) + +## Testing a New Site + +1. Run with `--journey full` first to get complete picture +2. Check `gtm_audit_report.json` for issues +3. Focus on specific areas with targeted journey types +4. Use `--container GTM-XXXXXX` to validate specific container + +## Common Tasks + +### Add support for new tag platform +```python +# In TAG_DESTINATIONS dict +"NewPlatform": [ + r"tracking\.newplatform\.com", + r"pixel\.newplatform\.com", +], +``` + +### Add custom form field detection +```python +# In FormAnalyzer.discover_forms() +# Add new field types or selectors +``` + +### Extend checkout flow for specific platform +```python +# In CheckoutFlowAnalyzer.detect_checkout_elements() +# Add platform-specific selectors +``` diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-audit/README.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/README.md new file mode 100644 index 0000000..9ada757 --- /dev/null +++ b/ourdigital-custom-skills/13-ourdigital-gtm-audit/README.md @@ -0,0 +1,90 @@ +# OurDigital GTM Audit + +Lightweight Google Tag Manager audit toolkit powered by Playwright. + +> **Note**: For comprehensive GTM management including dataLayer tag generation, see [14-ourdigital-gtm-manager](../14-ourdigital-gtm-manager/). + +## Features + +- **Container Detection**: Verify GTM installation, position, and duplicates +- **DataLayer Validation**: Event structure, types, sequence checking +- **Form Tracking**: Form discovery, field analysis, event verification +- **E-commerce Checkout**: Full funnel flow simulation and validation +- **Multi-Platform**: GA4, Meta Pixel, LinkedIn, Google Ads, Kakao, Naver + +## Installation + +```bash +# Clone or download +cd gtm-audit-claude-code + +# Install dependencies +pip install -r requirements.txt + +# Install Playwright browsers +playwright install chromium +``` + +## Usage + +```bash +# Full audit +python gtm_audit.py --url "https://yoursite.com" --journey full + +# Specific container validation +python gtm_audit.py --url "https://yoursite.com" --container "GTM-XXXXXX" + +# Form tracking only +python gtm_audit.py --url "https://yoursite.com/contact" --journey form + +# E-commerce checkout +python gtm_audit.py --url "https://yoursite.com/cart" --journey checkout +``` + +## Options + +| Option | Description | Default | +|--------|-------------|---------| +| `--url` | Target URL (required) | - | +| `--container` | Expected GTM container ID | None | +| `--journey` | Audit type: pageview, scroll, form, checkout, datalayer, full | full | +| `--output` | Output file path | gtm_audit_report.json | +| `--timeout` | Page load timeout (ms) | 30000 | +| `--headless` | Run browser headless | True | + +## Output + +Generates JSON report with: +- Container status +- DataLayer events and validation issues +- Form analysis and tracking readiness +- Checkout flow analysis +- Network requests by destination +- Recommendations and checklist + +## Using with Claude Code + +This project includes a `CLAUDE.md` file optimized for use with Claude Code. + +```bash +# In your terminal +claude + +# Then ask Claude to run audits +> Run a GTM audit on https://example.com +> Check the form tracking on https://example.com/contact +> Analyze the checkout flow issues in the latest report +``` + +## Documentation + +See `docs/` folder for: +- GA4 event specifications +- E-commerce dataLayer schemas +- Form tracking patterns +- Checkout flow sequences +- Common issues and fixes + +## License + +MIT diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/checkout_flow.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/checkout_flow.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/checkout_flow.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/checkout_flow.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/common_issues.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/common_issues.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/common_issues.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/common_issues.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/datalayer_validation.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/datalayer_validation.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/datalayer_validation.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/datalayer_validation.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/ecommerce_schema.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/ecommerce_schema.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/ecommerce_schema.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/ecommerce_schema.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/form_tracking.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/form_tracking.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/form_tracking.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/form_tracking.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/ga4_events.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/ga4_events.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/ga4_events.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/ga4_events.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/report_template.md b/ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/report_template.md similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/docs/report_template.md rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/docs/report_template.md diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-audit/gtm_audit.py b/ourdigital-custom-skills/13-ourdigital-gtm-audit/gtm_audit.py new file mode 100644 index 0000000..c0fd384 --- /dev/null +++ b/ourdigital-custom-skills/13-ourdigital-gtm-audit/gtm_audit.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python3 +""" +GTM Audit Script - Comprehensive Google Tag Manager audit with form tracking, +e-commerce checkout flow, and advanced dataLayer validation. + +Usage: + python gtm_audit.py --url "https://example.com" [options] + +Options: + --url Target URL to audit (required) + --container Expected GTM container ID (e.g., GTM-XXXXXX) + --journey Journey type: pageview, scroll, click, form, checkout, datalayer, full + --output Output file path (default: gtm_audit_report.json) + --timeout Page load timeout in ms (default: 30000) + --headless Run in headless mode (default: True) +""" + +import argparse +import json +import re +import sys +from datetime import datetime +from urllib.parse import urlparse, parse_qs +from playwright.sync_api import sync_playwright + +# Tag destination patterns +TAG_DESTINATIONS = { + "GA4": [ + r"google-analytics\.com/g/collect", + r"analytics\.google\.com/g/collect", + ], + "Universal Analytics": [ + r"google-analytics\.com/collect", + r"google-analytics\.com/r/collect", + ], + "Google Ads": [ + r"googleads\.g\.doubleclick\.net", + r"google\.com/pagead", + r"googleadservices\.com/pagead", + ], + "Meta Pixel": [ + r"facebook\.com/tr", + r"connect\.facebook\.net", + ], + "LinkedIn": [ + r"px\.ads\.linkedin\.com", + r"snap\.licdn\.com", + ], + "TikTok": [ + r"analytics\.tiktok\.com", + ], + "Twitter/X": [ + r"ads-twitter\.com", + r"t\.co/i/adsct", + ], + "Kakao": [ + r"pixel\.kakao\.com", + ], + "Naver": [ + r"wcs\.naver\.com", + ], +} + +# GA4 Required Parameters by Event +GA4_EVENT_REQUIREMENTS = { + "purchase": { + "required": ["transaction_id", "value", "currency"], + "items_required": ["item_id", "item_name"], + }, + "add_to_cart": { + "required": ["currency", "value"], + "items_required": ["item_id", "item_name"], + }, + "begin_checkout": { + "required": ["currency", "value"], + "items_required": ["item_id", "item_name"], + }, + "add_shipping_info": { + "required": ["currency", "value"], + "recommended": ["shipping_tier"], + }, + "add_payment_info": { + "required": ["currency", "value"], + "recommended": ["payment_type"], + }, + "view_item": { + "required": ["currency", "value"], + "items_required": ["item_id", "item_name"], + }, + "view_cart": { + "required": ["currency", "value"], + "items_required": ["item_id", "item_name"], + }, + "generate_lead": { + "recommended": ["currency", "value"], + }, + "form_submit": { + "recommended": ["form_id", "form_name"], + }, +} + +# Checkout flow sequence +CHECKOUT_SEQUENCE = [ + "view_cart", + "begin_checkout", + "add_shipping_info", + "add_payment_info", + "purchase", +] + + +class DataLayerValidator: + """Advanced dataLayer validation and monitoring.""" + + def __init__(self): + self.events = [] + self.issues = [] + self.snapshots = [] + + def validate_event(self, event_data): + """Validate a single dataLayer event against GA4 specs.""" + issues = [] + event_name = event_data.get("event") + + if not event_name: + return issues + + # Check if event has requirements + if event_name in GA4_EVENT_REQUIREMENTS: + reqs = GA4_EVENT_REQUIREMENTS[event_name] + ecommerce = event_data.get("ecommerce", {}) + + # Check required fields + for field in reqs.get("required", []): + if field not in ecommerce and field not in event_data: + issues.append({ + "type": "missing_required", + "event": event_name, + "field": field, + "message": f"Missing required field: {field}", + }) + + # Check items array + items = ecommerce.get("items", []) + if reqs.get("items_required") and not items: + issues.append({ + "type": "missing_items", + "event": event_name, + "message": "E-commerce event missing 'items' array", + }) + + # Validate items structure + for i, item in enumerate(items): + for field in reqs.get("items_required", []): + if field not in item: + issues.append({ + "type": "item_missing_field", + "event": event_name, + "item_index": i, + "field": field, + "message": f"Item {i} missing required field: {field}", + }) + + # Check data types + if "value" in ecommerce: + if not isinstance(ecommerce["value"], (int, float)): + issues.append({ + "type": "wrong_type", + "event": event_name, + "field": "value", + "message": f"'value' should be number, got {type(ecommerce['value']).__name__}", + }) + + # Check transaction_id uniqueness hint + if event_name == "purchase" and "transaction_id" in ecommerce: + tid = ecommerce["transaction_id"] + if not tid or tid == "" or tid == "undefined": + issues.append({ + "type": "invalid_transaction_id", + "event": event_name, + "message": "transaction_id is empty or invalid", + }) + + return issues + + def validate_sequence(self, events): + """Validate checkout event sequence.""" + issues = [] + event_names = [e.get("event") for e in events if e.get("event")] + + # Find checkout events in order + checkout_events = [e for e in event_names if e in CHECKOUT_SEQUENCE] + + # Check sequence + last_idx = -1 + for event in checkout_events: + idx = CHECKOUT_SEQUENCE.index(event) + if idx < last_idx: + issues.append({ + "type": "sequence_error", + "message": f"Event '{event}' fired out of order", + }) + last_idx = idx + + return issues + + def check_ecommerce_clear(self, events): + """Check if ecommerce object is cleared before new pushes.""" + issues = [] + last_had_ecommerce = False + + for i, event in enumerate(events): + has_ecommerce = "ecommerce" in event + is_clear = event.get("ecommerce") is None + + if has_ecommerce and last_had_ecommerce and not is_clear: + # Previous had ecommerce, this has ecommerce, but no clear + issues.append({ + "type": "missing_ecommerce_clear", + "index": i, + "event": event.get("event"), + "message": "E-commerce data should be cleared before new push", + }) + + if has_ecommerce and not is_clear: + last_had_ecommerce = True + elif is_clear: + last_had_ecommerce = False + + return issues + + +class FormAnalyzer: + """Form discovery, analysis, and interaction tracking.""" + + def __init__(self, page): + self.page = page + self.forms = [] + self.interactions = [] + self.issues = [] + + def discover_forms(self): + """Find and analyze all forms on the page.""" + forms_data = self.page.evaluate(""" + () => { + const forms = document.querySelectorAll('form'); + return Array.from(forms).map((form, idx) => { + const fields = Array.from(form.querySelectorAll('input, select, textarea')); + return { + index: idx, + id: form.id || null, + name: form.name || null, + action: form.action || null, + method: form.method || 'get', + className: form.className || null, + fieldCount: fields.length, + fields: fields.map(field => ({ + type: field.type || field.tagName.toLowerCase(), + name: field.name || null, + id: field.id || null, + required: field.required || false, + placeholder: field.placeholder || null, + validation: field.pattern || null, + maxLength: field.maxLength > 0 ? field.maxLength : null, + })), + hasSubmitButton: form.querySelector('button[type="submit"], input[type="submit"]') !== null, + }; + }); + } + """) + + self.forms = forms_data + return forms_data + + def analyze_form_tracking_readiness(self): + """Check if forms are ready for GTM tracking.""" + issues = [] + + for form in self.forms: + # Check for identifiers + if not form["id"] and not form["name"]: + issues.append({ + "type": "form_no_identifier", + "form_index": form["index"], + "message": f"Form {form['index']} has no id or name attribute", + "recommendation": "Add id or name attribute for reliable form tracking", + }) + + # Check fields for tracking + for field in form["fields"]: + if field["type"] in ["text", "email", "tel"] and not field["name"] and not field["id"]: + issues.append({ + "type": "field_no_identifier", + "form_index": form["index"], + "field_type": field["type"], + "message": "Input field missing name/id for tracking", + }) + + # Check for submit button + if not form["hasSubmitButton"]: + issues.append({ + "type": "form_no_submit", + "form_index": form["index"], + "message": "Form has no submit button - may use JS submission", + "recommendation": "Verify form submission triggers dataLayer push", + }) + + self.issues = issues + return issues + + def simulate_form_interaction(self, form_index=0): + """Simulate user interaction with a form.""" + if form_index >= len(self.forms): + return {"error": "Form index out of range"} + + form = self.forms[form_index] + interactions = [] + + # Find form element + form_selector = f"form:nth-of-type({form_index + 1})" + if form["id"]: + form_selector = f"#{form['id']}" + elif form["name"]: + form_selector = f"form[name='{form['name']}']" + + try: + form_element = self.page.locator(form_selector) + + # Interact with each field + for field in form["fields"]: + field_selector = None + if field["id"]: + field_selector = f"#{field['id']}" + elif field["name"]: + field_selector = f"[name='{field['name']}']" + + if not field_selector: + continue + + try: + field_element = self.page.locator(field_selector).first + + # Focus event + field_element.focus() + interactions.append({ + "action": "focus", + "field": field["name"] or field["id"], + "timestamp": datetime.now().isoformat(), + }) + self.page.wait_for_timeout(200) + + # Fill based on type + test_values = { + "text": "Test User", + "email": "test@example.com", + "tel": "010-1234-5678", + "number": "100", + "password": "TestPass123!", + } + + if field["type"] in test_values: + field_element.fill(test_values[field["type"]]) + interactions.append({ + "action": "input", + "field": field["name"] or field["id"], + "type": field["type"], + "timestamp": datetime.now().isoformat(), + }) + self.page.wait_for_timeout(200) + + # Blur event + field_element.blur() + interactions.append({ + "action": "blur", + "field": field["name"] or field["id"], + "timestamp": datetime.now().isoformat(), + }) + + except Exception as e: + interactions.append({ + "action": "error", + "field": field["name"] or field["id"], + "error": str(e), + }) + + self.interactions = interactions + return interactions + + except Exception as e: + return {"error": str(e)} + + def check_form_events(self, datalayer_events): + """Check if expected form events are in dataLayer.""" + expected_events = ["form_start", "form_submit", "generate_lead"] + found_events = [] + missing_events = [] + + event_names = [e.get("event") for e in datalayer_events] + + for expected in expected_events: + if expected in event_names: + found_events.append(expected) + else: + missing_events.append(expected) + + return { + "found": found_events, + "missing": missing_events, + "recommendation": "Consider implementing: " + ", ".join(missing_events) if missing_events else None, + } + + +class CheckoutFlowAnalyzer: + """E-commerce checkout flow simulation and validation.""" + + def __init__(self, page): + self.page = page + self.steps_completed = [] + self.events_captured = [] + self.issues = [] + + def detect_checkout_elements(self): + """Find checkout-related elements on page.""" + elements = self.page.evaluate(""" + () => { + const selectors = { + cart: [ + '[class*="cart"]', '[id*="cart"]', + '[class*="basket"]', '[id*="basket"]', + ], + checkout: [ + '[class*="checkout"]', '[id*="checkout"]', + 'button:has-text("Checkout")', 'a:has-text("Checkout")', + 'button:has-text("결제")', 'a:has-text("결제")', + ], + addToCart: [ + 'button:has-text("Add to Cart")', 'button:has-text("Add to Bag")', + 'button:has-text("장바구니")', 'button:has-text("담기")', + '[class*="add-to-cart"]', '[id*="add-to-cart"]', + ], + quantity: [ + '[class*="quantity"]', '[name*="quantity"]', + '[class*="qty"]', '[name*="qty"]', + ], + removeItem: [ + '[class*="remove"]', 'button:has-text("Remove")', + 'button:has-text("삭제")', '[class*="delete"]', + ], + promoCode: [ + '[name*="promo"]', '[name*="coupon"]', '[id*="coupon"]', + '[placeholder*="promo"]', '[placeholder*="coupon"]', + ], + }; + + const found = {}; + for (const [type, selectorList] of Object.entries(selectors)) { + found[type] = []; + for (const sel of selectorList) { + try { + const elements = document.querySelectorAll(sel); + elements.forEach(el => { + found[type].push({ + selector: sel, + tag: el.tagName.toLowerCase(), + text: el.textContent?.slice(0, 50) || null, + visible: el.offsetParent !== null, + }); + }); + } catch(e) {} + } + } + return found; + } + """) + return elements + + def simulate_add_to_cart(self): + """Attempt to simulate add-to-cart action.""" + try: + # Try common add-to-cart selectors + selectors = [ + 'button:has-text("Add to Cart")', + 'button:has-text("Add to Bag")', + 'button:has-text("장바구니")', + '[class*="add-to-cart"]:visible', + '[id*="add-to-cart"]:visible', + ] + + for selector in selectors: + try: + btn = self.page.locator(selector).first + if btn.is_visible(): + btn.click() + self.steps_completed.append({ + "step": "add_to_cart", + "selector": selector, + "timestamp": datetime.now().isoformat(), + }) + self.page.wait_for_timeout(1500) + return True + except: + continue + + return False + except Exception as e: + self.issues.append({"step": "add_to_cart", "error": str(e)}) + return False + + def simulate_begin_checkout(self): + """Attempt to click checkout button.""" + try: + selectors = [ + 'button:has-text("Checkout")', + 'a:has-text("Checkout")', + 'button:has-text("결제하기")', + 'button:has-text("주문하기")', + '[class*="checkout-btn"]:visible', + ] + + for selector in selectors: + try: + btn = self.page.locator(selector).first + if btn.is_visible(): + btn.click() + self.steps_completed.append({ + "step": "begin_checkout", + "selector": selector, + "timestamp": datetime.now().isoformat(), + }) + self.page.wait_for_timeout(2000) + return True + except: + continue + + return False + except Exception as e: + self.issues.append({"step": "begin_checkout", "error": str(e)}) + return False + + def validate_checkout_events(self, datalayer_events): + """Validate checkout-related events in dataLayer.""" + results = { + "events_found": [], + "events_missing": [], + "sequence_valid": True, + "issues": [], + } + + event_names = [e.get("event") for e in datalayer_events] + + # Check each checkout step + for step in CHECKOUT_SEQUENCE: + if step in event_names: + results["events_found"].append(step) + + # Validate event parameters + for event in datalayer_events: + if event.get("event") == step: + validator = DataLayerValidator() + issues = validator.validate_event(event) + results["issues"].extend(issues) + else: + results["events_missing"].append(step) + + # Check sequence + found_sequence = [e for e in event_names if e in CHECKOUT_SEQUENCE] + expected_order = [e for e in CHECKOUT_SEQUENCE if e in found_sequence] + + if found_sequence != expected_order: + results["sequence_valid"] = False + results["issues"].append({ + "type": "sequence_error", + "message": f"Events out of order. Found: {found_sequence}, Expected: {expected_order}", + }) + + return results + + +class GTMAuditor: + """Main GTM audit orchestrator.""" + + def __init__(self, url, container_id=None, timeout=30000, headless=True): + self.url = url + self.expected_container = container_id + self.timeout = timeout + self.headless = headless + self.report = { + "audit_metadata": { + "url": url, + "timestamp": datetime.now().isoformat(), + "expected_container": container_id, + }, + "container_status": {}, + "datalayer_analysis": { + "events": [], + "validation_issues": [], + "sequence_issues": [], + }, + "form_analysis": { + "forms_found": [], + "tracking_issues": [], + "events_status": {}, + }, + "checkout_analysis": { + "elements_found": {}, + "events_status": {}, + "flow_issues": [], + }, + "network_requests": [], + "tags_fired": [], + "issues": [], + "recommendations": [], + "checklist": {}, + } + self.network_requests = [] + self.datalayer_history = [] + self.page = None + + def _setup_network_monitoring(self, page): + """Intercept and log network requests to tag destinations.""" + def handle_request(request): + url = request.url + for destination, patterns in TAG_DESTINATIONS.items(): + for pattern in patterns: + if re.search(pattern, url): + parsed = urlparse(url) + params = parse_qs(parsed.query) + self.network_requests.append({ + "destination": destination, + "url": url[:200], + "method": request.method, + "params": {k: v[0] if len(v) == 1 else v for k, v in params.items()}, + "timestamp": datetime.now().isoformat(), + }) + break + + page.on("request", handle_request) + + def _setup_datalayer_monitoring(self, page): + """Inject dataLayer monitoring script.""" + page.evaluate(""" + () => { + window.__gtmAuditEvents = []; + const originalPush = window.dataLayer.push; + window.dataLayer.push = function() { + const result = originalPush.apply(this, arguments); + for (let i = 0; i < arguments.length; i++) { + window.__gtmAuditEvents.push({ + data: JSON.parse(JSON.stringify(arguments[i])), + timestamp: new Date().toISOString() + }); + } + return result; + }; + } + """) + + def _capture_datalayer(self, page): + """Capture current dataLayer state.""" + try: + datalayer = page.evaluate(""" + () => { + if (typeof window.dataLayer !== 'undefined') { + return JSON.parse(JSON.stringify(window.dataLayer)); + } + return null; + } + """) + return datalayer + except Exception as e: + return {"error": str(e)} + + def _capture_monitored_events(self, page): + """Capture events logged by our monitoring.""" + try: + events = page.evaluate(""" + () => window.__gtmAuditEvents || [] + """) + return events + except: + return [] + + def _check_gtm_container(self, page): + """Verify GTM container installation.""" + result = page.evaluate(""" + () => { + const scripts = document.querySelectorAll('script'); + const gtmInfo = { + installed: false, + containers: [], + position: null, + noscript: false, + dataLayerInit: false, + dataLayerInitBeforeGTM: false, + }; + + gtmInfo.dataLayerInit = typeof window.dataLayer !== 'undefined' && + Array.isArray(window.dataLayer); + + let gtmScriptIndex = -1; + let dataLayerInitIndex = -1; + + scripts.forEach((script, index) => { + const src = script.src || ''; + const innerHTML = script.innerHTML || ''; + + // Check for dataLayer init + if (innerHTML.includes('dataLayer') && innerHTML.includes('[]')) { + dataLayerInitIndex = index; + } + + const gtmMatch = src.match(/gtm\\.js\\?id=(GTM-[A-Z0-9]+)/); + if (gtmMatch) { + gtmInfo.installed = true; + gtmInfo.containers.push(gtmMatch[1]); + gtmInfo.position = script.closest('head') ? 'head' : 'body'; + gtmScriptIndex = index; + } + + const inlineMatch = innerHTML.match(/GTM-[A-Z0-9]+/g); + if (inlineMatch) { + gtmInfo.installed = true; + inlineMatch.forEach(id => { + if (!gtmInfo.containers.includes(id)) { + gtmInfo.containers.push(id); + } + }); + } + }); + + gtmInfo.dataLayerInitBeforeGTM = dataLayerInitIndex < gtmScriptIndex && dataLayerInitIndex !== -1; + + const noscripts = document.querySelectorAll('noscript'); + noscripts.forEach(ns => { + if (ns.innerHTML.includes('googletagmanager.com/ns.html')) { + gtmInfo.noscript = true; + } + }); + + return gtmInfo; + } + """) + + status = { + "installed": result["installed"], + "containers": result["containers"], + "position": result["position"], + "noscript_present": result["noscript"], + "datalayer_initialized": result["dataLayerInit"], + "datalayer_init_before_gtm": result["dataLayerInitBeforeGTM"], + "issues": [], + } + + if not result["installed"]: + status["issues"].append("GTM container not detected") + self.report["issues"].append({ + "severity": "critical", + "type": "container_missing", + "message": "GTM container script not found on page", + }) + + if len(result["containers"]) > 1: + status["issues"].append(f"Multiple containers: {result['containers']}") + self.report["issues"].append({ + "severity": "warning", + "type": "multiple_containers", + "message": f"Multiple GTM containers found: {', '.join(result['containers'])}", + }) + + if self.expected_container and self.expected_container not in result["containers"]: + self.report["issues"].append({ + "severity": "error", + "type": "container_mismatch", + "message": f"Expected {self.expected_container}, found {result['containers']}", + }) + + if result["position"] == "body": + self.report["issues"].append({ + "severity": "warning", + "type": "script_position", + "message": "GTM script in body - may delay tag firing", + }) + + if not result["dataLayerInitBeforeGTM"]: + self.report["issues"].append({ + "severity": "warning", + "type": "datalayer_order", + "message": "dataLayer should be initialized before GTM script", + }) + + self.report["container_status"] = status + return status + + def _simulate_scroll(self, page): + """Simulate scroll to trigger scroll-depth tags.""" + page.evaluate(""" + () => { + const heights = [0.25, 0.5, 0.75, 0.9, 1.0]; + const docHeight = document.documentElement.scrollHeight; + heights.forEach((pct, i) => { + setTimeout(() => { + window.scrollTo(0, docHeight * pct); + }, i * 500); + }); + } + """) + page.wait_for_timeout(3000) + + def _run_form_audit(self, page): + """Execute form analysis.""" + print("📝 Analyzing forms...") + + form_analyzer = FormAnalyzer(page) + forms = form_analyzer.discover_forms() + tracking_issues = form_analyzer.analyze_form_tracking_readiness() + + self.report["form_analysis"]["forms_found"] = forms + self.report["form_analysis"]["tracking_issues"] = tracking_issues + + if forms: + print(f" Found {len(forms)} form(s)") + # Simulate interaction with first form + interactions = form_analyzer.simulate_form_interaction(0) + self.report["form_analysis"]["interactions"] = interactions + + # Allow time for events + page.wait_for_timeout(2000) + + # Check form events + datalayer = self._capture_datalayer(page) + if datalayer: + events_status = form_analyzer.check_form_events(datalayer) + self.report["form_analysis"]["events_status"] = events_status + else: + print(" No forms found on page") + + def _run_checkout_audit(self, page): + """Execute e-commerce checkout flow analysis.""" + print("🛒 Analyzing checkout flow...") + + checkout_analyzer = CheckoutFlowAnalyzer(page) + elements = checkout_analyzer.detect_checkout_elements() + + self.report["checkout_analysis"]["elements_found"] = elements + + # Log what we found + for element_type, found in elements.items(): + if found: + print(f" Found {len(found)} {element_type} element(s)") + + def _run_datalayer_audit(self, page): + """Execute deep dataLayer analysis.""" + print("📊 Analyzing dataLayer...") + + datalayer = self._capture_datalayer(page) + monitored_events = self._capture_monitored_events(page) + + if not datalayer: + self.report["datalayer_analysis"]["issues"] = ["dataLayer not found"] + return + + validator = DataLayerValidator() + + # Validate each event + for event in datalayer: + if isinstance(event, dict): + issues = validator.validate_event(event) + if issues: + self.report["datalayer_analysis"]["validation_issues"].extend(issues) + + # Check sequence + sequence_issues = validator.validate_sequence(datalayer) + self.report["datalayer_analysis"]["sequence_issues"] = sequence_issues + + # Check ecommerce clearing + clear_issues = validator.check_ecommerce_clear(datalayer) + self.report["datalayer_analysis"]["validation_issues"].extend(clear_issues) + + # Store events + events = [] + for i, item in enumerate(datalayer): + if isinstance(item, dict) and item.get("event"): + events.append({ + "index": i, + "event": item.get("event"), + "has_ecommerce": "ecommerce" in item, + "params": list(item.keys()), + }) + + self.report["datalayer_analysis"]["events"] = events + print(f" Found {len(events)} events in dataLayer") + + def _generate_recommendations(self): + """Generate recommendations based on findings.""" + recs = [] + + for issue in self.report["issues"]: + if issue["type"] == "container_missing": + recs.append({ + "priority": "high", + "action": "Install GTM container", + "details": "Add GTM snippet to
section", + }) + elif issue["type"] == "datalayer_order": + recs.append({ + "priority": "medium", + "action": "Initialize dataLayer before GTM", + "details": "Add 'window.dataLayer = window.dataLayer || [];' before GTM", + }) + + # Form recommendations + if not self.report["form_analysis"]["forms_found"]: + pass # No forms to track + elif self.report["form_analysis"].get("events_status", {}).get("missing"): + missing = self.report["form_analysis"]["events_status"]["missing"] + recs.append({ + "priority": "medium", + "action": "Implement form tracking events", + "details": f"Missing events: {', '.join(missing)}", + }) + + # DataLayer recommendations + validation_issues = self.report["datalayer_analysis"].get("validation_issues", []) + if validation_issues: + recs.append({ + "priority": "high", + "action": "Fix dataLayer validation issues", + "details": f"{len(validation_issues)} issue(s) found in event structure", + }) + + # Tag coverage + destinations = set(r["destination"] for r in self.network_requests) + if "GA4" not in destinations: + recs.append({ + "priority": "high", + "action": "Verify GA4 implementation", + "details": "No GA4 requests detected", + }) + + self.report["recommendations"] = recs + + def _generate_checklist(self): + """Generate audit checklist.""" + self.report["checklist"] = { + "container_health": { + "gtm_installed": self.report["container_status"].get("installed", False), + "correct_container": self.expected_container in self.report["container_status"].get("containers", []) if self.expected_container else True, + "no_duplicates": len(self.report["container_status"].get("containers", [])) <= 1, + "correct_position": self.report["container_status"].get("position") == "head", + "datalayer_init_order": self.report["container_status"].get("datalayer_init_before_gtm", False), + }, + "datalayer_quality": { + "initialized": self.report["container_status"].get("datalayer_initialized", False), + "events_present": len(self.report["datalayer_analysis"].get("events", [])) > 0, + "no_validation_errors": len(self.report["datalayer_analysis"].get("validation_issues", [])) == 0, + "correct_sequence": len(self.report["datalayer_analysis"].get("sequence_issues", [])) == 0, + }, + "form_tracking": { + "forms_identifiable": all( + f.get("id") or f.get("name") + for f in self.report["form_analysis"].get("forms_found", []) + ) if self.report["form_analysis"].get("forms_found") else True, + "form_events_present": len( + self.report["form_analysis"].get("events_status", {}).get("found", []) + ) > 0 if self.report["form_analysis"].get("forms_found") else True, + }, + "tag_firing": { + "ga4_active": any(r["destination"] == "GA4" for r in self.network_requests), + "requests_captured": len(self.network_requests) > 0, + }, + } + + def run_audit(self, journey="pageview"): + """Execute the full audit workflow.""" + print(f"🔍 Starting GTM audit for: {self.url}") + print(f" Journey type: {journey}") + + with sync_playwright() as p: + browser = p.chromium.launch(headless=self.headless) + context = browser.new_context( + viewport={"width": 1920, "height": 1080}, + user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) GTMAudit/1.0" + ) + page = context.new_page() + self.page = page + + self._setup_network_monitoring(page) + + try: + print("📄 Loading page...") + page.goto(self.url, timeout=self.timeout, wait_until="networkidle") + page.wait_for_timeout(2000) + + # Setup dataLayer monitoring after page load + try: + self._setup_datalayer_monitoring(page) + except: + pass + + print("🏷️ Checking GTM container...") + self._check_gtm_container(page) + + # Run journey-specific audits + if journey in ["scroll", "pageview", "full"]: + print("📜 Simulating scroll...") + self._simulate_scroll(page) + + if journey in ["form", "full"]: + self._run_form_audit(page) + + if journey in ["checkout", "full"]: + self._run_checkout_audit(page) + + if journey in ["datalayer", "full"]: + self._run_datalayer_audit(page) + + # Always do basic dataLayer capture + page.wait_for_timeout(2000) + self._run_datalayer_audit(page) + + # Store network requests + self.report["network_requests"] = self.network_requests + self.report["tags_fired"] = list(set(r["destination"] for r in self.network_requests)) + + except Exception as e: + self.report["issues"].append({ + "severity": "critical", + "type": "audit_error", + "message": str(e), + }) + finally: + browser.close() + + self._generate_recommendations() + self._generate_checklist() + + print("✅ Audit complete!") + return self.report + + def save_report(self, filepath): + """Save report to JSON file.""" + with open(filepath, "w", encoding="utf-8") as f: + json.dump(self.report, f, indent=2, ensure_ascii=False) + print(f"📝 Report saved to: {filepath}") + + def print_summary(self): + """Print audit summary to console.""" + print("\n" + "="*60) + print("📋 GTM AUDIT SUMMARY") + print("="*60) + + # Container + cs = self.report["container_status"] + print(f"\n🏷️ Container: {'✅ Installed' if cs.get('installed') else '❌ Not Found'}") + if cs.get("containers"): + print(f" IDs: {', '.join(cs['containers'])}") + + # DataLayer + dl = self.report["datalayer_analysis"] + print(f"\n📊 DataLayer:") + print(f" Events found: {len(dl.get('events', []))}") + print(f" Validation issues: {len(dl.get('validation_issues', []))}") + + # Forms + fa = self.report["form_analysis"] + if fa.get("forms_found"): + print(f"\n📝 Forms:") + print(f" Forms found: {len(fa['forms_found'])}") + print(f" Tracking issues: {len(fa.get('tracking_issues', []))}") + + # Tags + print(f"\n🔥 Tags Fired: {', '.join(self.report['tags_fired']) if self.report['tags_fired'] else 'None detected'}") + + # Issues + print(f"\n⚠️ Total Issues: {len(self.report['issues'])}") + for issue in self.report["issues"][:5]: + print(f" - [{issue['severity'].upper()}] {issue['message']}") + + # Recommendations + print(f"\n💡 Recommendations: {len(self.report['recommendations'])}") + for rec in self.report["recommendations"][:3]: + print(f" - [{rec['priority'].upper()}] {rec['action']}") + + print("\n" + "="*60) + + +def main(): + parser = argparse.ArgumentParser(description="GTM Audit Tool") + parser.add_argument("--url", required=True, help="Target URL to audit") + parser.add_argument("--container", help="Expected GTM container ID (e.g., GTM-XXXXXX)") + parser.add_argument("--journey", default="full", + choices=["pageview", "scroll", "click", "form", "checkout", "datalayer", "full"], + help="Journey type to simulate") + parser.add_argument("--output", default="gtm_audit_report.json", help="Output file path") + parser.add_argument("--timeout", type=int, default=30000, help="Page load timeout (ms)") + parser.add_argument("--headless", action="store_true", default=True, help="Run headless") + + args = parser.parse_args() + + auditor = GTMAuditor( + url=args.url, + container_id=args.container, + timeout=args.timeout, + headless=args.headless, + ) + + report = auditor.run_audit(journey=args.journey) + auditor.save_report(args.output) + auditor.print_summary() + + +if __name__ == "__main__": + main() diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/requirements.txt b/ourdigital-custom-skills/13-ourdigital-gtm-audit/requirements.txt similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/requirements.txt rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/requirements.txt diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/setup.sh b/ourdigital-custom-skills/13-ourdigital-gtm-audit/setup.sh similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/setup.sh rename to ourdigital-custom-skills/13-ourdigital-gtm-audit/setup.sh diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/CLAUDE.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/CLAUDE.md similarity index 97% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/CLAUDE.md rename to ourdigital-custom-skills/14-ourdigital-gtm-manager/CLAUDE.md index 514fc84..6c4da6b 100644 --- a/ourdigital-custom-skills/13-ourdigital-gtm-manager/CLAUDE.md +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/CLAUDE.md @@ -2,6 +2,8 @@ Comprehensive Google Tag Manager management toolkit - audit, analyze, and generate dataLayer implementations. +> **Note**: For lightweight audit-only functionality, see [13-ourdigital-gtm-audit](../13-ourdigital-gtm-audit/). + ## Project Overview This tool provides two main capabilities: diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/README.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/README.md similarity index 97% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/README.md rename to ourdigital-custom-skills/14-ourdigital-gtm-manager/README.md index 9c47230..2aea0e4 100644 --- a/ourdigital-custom-skills/13-ourdigital-gtm-manager/README.md +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/README.md @@ -2,6 +2,8 @@ Comprehensive Google Tag Manager management toolkit powered by Playwright. +> **Note**: For lightweight audit-only functionality, see [13-ourdigital-gtm-audit](../13-ourdigital-gtm-audit/). + ## Features - **Audit Mode**: Validate GTM installations, dataLayer events, forms, and checkout flows diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/checkout_flow.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/checkout_flow.md new file mode 100644 index 0000000..3204807 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/checkout_flow.md @@ -0,0 +1,237 @@ +# E-commerce Checkout Flow Reference + +## Complete Checkout Event Sequence + +``` +view_cart → begin_checkout → add_shipping_info → add_payment_info → purchase +``` + +Each step must fire in order with consistent item data. + +## Event Details + +### 1. view_cart +When user views cart page. + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "view_cart", + ecommerce: { + currency: "KRW", + value: 125000, + items: [{ + item_id: "SKU_001", + item_name: "Blue T-Shirt", + price: 45000, + quantity: 2, + item_brand: "Brand", + item_category: "Apparel" + }, { + item_id: "SKU_002", + item_name: "Black Jeans", + price: 35000, + quantity: 1 + }] + } +}); +``` + +### 2. begin_checkout +When user initiates checkout process. + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "begin_checkout", + ecommerce: { + currency: "KRW", + value: 125000, + coupon: "SUMMER10", + items: [/* same items as view_cart */] + } +}); +``` + +### 3. add_shipping_info +When user completes shipping step. + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "add_shipping_info", + ecommerce: { + currency: "KRW", + value: 125000, + coupon: "SUMMER10", + shipping_tier: "Express", // Required + items: [/* same items */] + } +}); +``` + +**shipping_tier values:** +- "Standard" / "일반배송" +- "Express" / "익일배송" +- "Same Day" / "당일배송" +- "Free" / "무료배송" +- "Store Pickup" / "매장픽업" + +### 4. add_payment_info +When user enters payment details. + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "add_payment_info", + ecommerce: { + currency: "KRW", + value: 125000, + coupon: "SUMMER10", + payment_type: "Credit Card", // Required + items: [/* same items */] + } +}); +``` + +**payment_type values:** +- "Credit Card" / "신용카드" +- "Debit Card" / "체크카드" +- "Bank Transfer" / "계좌이체" +- "Virtual Account" / "가상계좌" +- "Mobile Payment" / "휴대폰결제" +- "Kakao Pay" / "카카오페이" +- "Naver Pay" / "네이버페이" +- "Toss" / "토스" +- "PayPal" + +### 5. purchase +When transaction completes successfully. + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "purchase", + ecommerce: { + transaction_id: "T_20250115_001234", // Required, unique + value: 130500, // Required (total) + tax: 11863, + shipping: 5000, + currency: "KRW", // Required + coupon: "SUMMER10", + items: [{ + item_id: "SKU_001", + item_name: "Blue T-Shirt", + affiliation: "Online Store", + coupon: "SUMMER10", + discount: 4500, + price: 45000, + quantity: 2 + }] + } +}); +``` + +## Funnel Drop-off Analysis + +### Tracking Drop-offs +Monitor completion rate at each step: + +| Step | Event | Drop-off Indicator | +|------|-------|-------------------| +| Cart | view_cart | User leaves cart page | +| Checkout Start | begin_checkout | User doesn't proceed | +| Shipping | add_shipping_info | Address form abandoned | +| Payment | add_payment_info | Payment not completed | +| Complete | purchase | Transaction failed | + +### Implementing Drop-off Tracking + +```javascript +// Track checkout step viewed but not completed +let checkoutStep = 0; + +function trackCheckoutProgress(step) { + if (step > checkoutStep) { + checkoutStep = step; + } +} + +window.addEventListener('beforeunload', () => { + if (checkoutStep > 0 && checkoutStep < 5) { + dataLayer.push({ + event: 'checkout_abandon', + last_step: checkoutStep, + step_name: ['cart', 'checkout', 'shipping', 'payment', 'complete'][checkoutStep - 1] + }); + } +}); +``` + +## Value Consistency Check + +Ensure `value` matches across events: + +``` +view_cart.value = sum(items.price * items.quantity) +begin_checkout.value = view_cart.value +add_shipping_info.value = begin_checkout.value +add_payment_info.value = add_shipping_info.value +purchase.value = add_payment_info.value + shipping + tax - discount +``` + +## Common Issues + +### Duplicate Purchase Events +**Problem**: Same order tracked multiple times +**Solution**: +```javascript +// Check if already tracked +const txId = "T_12345"; +if (!sessionStorage.getItem('purchase_' + txId)) { + dataLayer.push({ event: 'purchase', ... }); + sessionStorage.setItem('purchase_' + txId, 'true'); +} +``` + +### Missing Items in Later Steps +**Problem**: Items present in view_cart but missing in purchase +**Solution**: Store cart data in session and reuse + +### Inconsistent Currency +**Problem**: Some events use USD, others KRW +**Solution**: Standardize currency across all events + +### Wrong Value Calculation +**Problem**: purchase.value doesn't include tax/shipping +**Solution**: +``` +purchase.value = subtotal + tax + shipping - discount +``` + +## Korean E-commerce Platforms + +### Cafe24 +Custom dataLayer variable names - check documentation + +### Shopify Korea +Standard GA4 format with `Shopify.checkout` object + +### WooCommerce +Use official GA4 plugin or custom implementation + +### Naver SmartStore +Separate Naver Analytics implementation required + +## Checkout Flow Checklist + +- [ ] view_cart fires on cart page load +- [ ] begin_checkout fires on checkout button click +- [ ] add_shipping_info includes shipping_tier +- [ ] add_payment_info includes payment_type +- [ ] purchase has unique transaction_id +- [ ] All events have consistent items array +- [ ] Currency is consistent across all events +- [ ] Value calculations are accurate +- [ ] ecommerce object cleared before each push +- [ ] Purchase event fires only once per order diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/common_issues.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/common_issues.md new file mode 100644 index 0000000..42b1af6 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/common_issues.md @@ -0,0 +1,211 @@ +# Common GTM Issues & Fixes + +## Container Issues + +### GTM Not Firing +**Symptoms**: No GTM requests in network tab +**Causes**: +1. Script blocked by ad blocker +2. Script placed after closing body tag +3. JavaScript error before GTM loads +4. Consent management blocking GTM + +**Fix**: +```html + + +``` + +### Multiple Containers Conflict +**Symptoms**: Duplicate events, inconsistent data +**Causes**: +1. Legacy container not removed +2. Different teams installed separate containers +3. Theme/plugin auto-installed GTM + +**Fix**: +1. Audit all containers in source +2. Consolidate to single container +3. Use GTM environments for staging/prod + +### Container ID Mismatch +**Symptoms**: Tags not firing, wrong property receiving data +**Causes**: +1. Dev/staging container on production +2. Copy-paste error during installation + +**Fix**: Verify container ID matches GTM account + +--- + +## DataLayer Issues + +### DataLayer Not Initialized +**Symptoms**: First push events lost +**Code Error**: +```javascript +// Wrong - GTM loads before dataLayer exists + +dataLayer.push({...}); +``` + +**Fix**: +```javascript +// Correct - Initialize dataLayer first + + +``` + +### Case Sensitivity Issues +**Symptoms**: Triggers not matching +**Example**: +```javascript +// DataLayer pushes "AddToCart" +dataLayer.push({ event: "AddToCart" }); + +// But GTM trigger looks for "addToCart" - won't match! +``` + +**Fix**: Standardize event naming (recommend lowercase with underscores) + +### Wrong Data Types +**Symptoms**: Calculations wrong in GA4, missing data +**Example**: +```javascript +// Wrong - price as string +dataLayer.push({ ecommerce: { value: "29.99" }}); + +// Correct - price as number +dataLayer.push({ ecommerce: { value: 29.99 }}); +``` + +### Timing Issues +**Symptoms**: Events fire before data available +**Cause**: DataLayer push happens after tag fires + +**Fix**: Use "Custom Event" trigger instead of "Page View" + +--- + +## Tag Issues + +### Tag Not Firing + +**Checklist**: +1. ✓ Trigger conditions met? +2. ✓ Trigger enabled? +3. ✓ Tag not paused? +4. ✓ No blocking triggers active? +5. ✓ Consent mode not blocking? + +**Debug Steps**: +1. GTM Preview > Check Tags Fired +2. Verify trigger shows green check +3. Check Variables tab for expected values + +### Duplicate Tag Firing +**Symptoms**: Events counted 2x in GA4 +**Causes**: +1. Multiple triggers on same action +2. Page re-renders triggering again +3. SPA virtual pageviews firing multiple times + +**Fix**: +1. Add "Once per event" tag firing option +2. Use trigger groups to control firing +3. Add conditions to prevent re-firing + +### Wrong Parameters Sent +**Symptoms**: Data appears in wrong fields in GA4 +**Debug**: +1. GTM Preview > Tags > Show fired tag +2. Check "Values" sent with tag +3. Compare with expected parameters + +--- + +## E-commerce Issues + +### Missing Transaction ID +**Symptoms**: Duplicate purchases counted +**Fix**: Ensure unique `transaction_id` generated server-side + +### Items Array Empty +**Symptoms**: Revenue tracked but no products +**Check**: `ecommerce.items` array populated + +### Value Mismatch +**Symptoms**: Revenue doesn't match actual +**Causes**: +1. Tax/shipping included inconsistently +2. Currency conversion issues +3. Discount applied incorrectly + +### Purchase Event Fires Multiple Times +**Symptoms**: Same order tracked 2-3x +**Causes**: +1. Page refresh on confirmation +2. Browser back button +3. Email link revisit + +**Fix**: +```javascript +// Check if already tracked +if (!sessionStorage.getItem('purchase_' + transaction_id)) { + dataLayer.push({ event: 'purchase', ... }); + sessionStorage.setItem('purchase_' + transaction_id, 'true'); +} +``` + +--- + +## Consent Mode Issues + +### Tags Blocked by Consent +**Symptoms**: Tags show "Blocked by consent" in Preview +**Fix**: +1. Verify consent mode implementation +2. Check default consent state +3. Test with consent granted + +### Consent Not Updating +**Symptoms**: Tags stay blocked after user accepts +**Fix**: Verify `gtag('consent', 'update', {...})` fires on accept + +--- + +## SPA (Single Page App) Issues + +### Pageviews Not Tracking Navigation +**Symptoms**: Only initial pageview tracked +**Cause**: No page reload on route change + +**Fix**: Implement History Change trigger or custom event: +```javascript +// On route change +dataLayer.push({ + event: 'virtual_pageview', + page_path: newPath, + page_title: newTitle +}); +``` + +### Events Fire on Old Page Data +**Symptoms**: Wrong page_path in events +**Fix**: Update page variables before event push + +--- + +## Performance Issues + +### Tags Slowing Page Load +**Symptoms**: High LCP, slow TTI +**Causes**: +1. Too many synchronous tags +2. Large third-party scripts +3. Tags in wrong firing sequence + +**Fix**: +1. Use tag sequencing +2. Load non-critical tags on Window Loaded +3. Defer marketing tags diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/datalayer_validation.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/datalayer_validation.md new file mode 100644 index 0000000..65336dc --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/datalayer_validation.md @@ -0,0 +1,287 @@ +# DataLayer Validation Reference + +## DataLayer Structure Basics + +### Proper Initialization +```javascript +// Must appear BEFORE GTM script + + +``` + +### 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'); +``` diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ecommerce_schema.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ecommerce_schema.md new file mode 100644 index 0000000..7499ad1 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ecommerce_schema.md @@ -0,0 +1,216 @@ +# E-commerce DataLayer Schema Reference + +## GA4 E-commerce Structure + +### Items Array Schema +Every e-commerce event requires an `items` array: + +```javascript +items: [{ + // Required + item_id: "SKU_12345", + item_name: "Blue T-Shirt", + + // Recommended + affiliation: "Store Name", + coupon: "SUMMER_SALE", + discount: 5.00, + index: 0, + item_brand: "Brand Name", + item_category: "Apparel", + item_category2: "Men", + item_category3: "Shirts", + item_category4: "T-Shirts", + item_category5: "Short Sleeve", + item_list_id: "related_products", + item_list_name: "Related Products", + item_variant: "Blue/Large", + location_id: "ChIJIQBpAG2ahYAR_6128GcTUEo", + price: 29.99, + quantity: 1 +}] +``` + +### Clear Previous E-commerce Data +Always clear before new e-commerce event: + +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "view_item", + ecommerce: { + // new data + } +}); +``` + +## Complete Purchase Flow + +### 1. Product List View +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "view_item_list", + ecommerce: { + item_list_id: "category_results", + item_list_name: "Category Results", + items: [ + { item_id: "SKU_001", item_name: "Product 1", index: 0, price: 29.99 }, + { item_id: "SKU_002", item_name: "Product 2", index: 1, price: 39.99 } + ] + } +}); +``` + +### 2. Product Click +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "select_item", + ecommerce: { + item_list_id: "category_results", + item_list_name: "Category Results", + items: [{ + item_id: "SKU_001", + item_name: "Product 1", + price: 29.99 + }] + } +}); +``` + +### 3. Product Detail View +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "view_item", + ecommerce: { + currency: "USD", + value: 29.99, + items: [{ + item_id: "SKU_001", + item_name: "Product 1", + item_brand: "Brand", + item_category: "Category", + price: 29.99, + quantity: 1 + }] + } +}); +``` + +### 4. Add to Cart +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "add_to_cart", + ecommerce: { + currency: "USD", + value: 29.99, + items: [{ + item_id: "SKU_001", + item_name: "Product 1", + price: 29.99, + quantity: 1 + }] + } +}); +``` + +### 5. View Cart +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "view_cart", + ecommerce: { + currency: "USD", + value: 59.98, + items: [ + { item_id: "SKU_001", item_name: "Product 1", price: 29.99, quantity: 1 }, + { item_id: "SKU_002", item_name: "Product 2", price: 29.99, quantity: 1 } + ] + } +}); +``` + +### 6. Begin Checkout +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "begin_checkout", + ecommerce: { + currency: "USD", + value: 59.98, + coupon: "DISCOUNT10", + items: [...] + } +}); +``` + +### 7. Add Shipping Info +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "add_shipping_info", + ecommerce: { + currency: "USD", + value: 59.98, + shipping_tier: "Standard", + items: [...] + } +}); +``` + +### 8. Add Payment Info +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "add_payment_info", + ecommerce: { + currency: "USD", + value: 59.98, + payment_type: "Credit Card", + items: [...] + } +}); +``` + +### 9. Purchase +```javascript +dataLayer.push({ ecommerce: null }); +dataLayer.push({ + event: "purchase", + ecommerce: { + transaction_id: "T_12345", + value: 65.97, + tax: 4.99, + shipping: 5.99, + currency: "USD", + coupon: "DISCOUNT10", + items: [{ + item_id: "SKU_001", + item_name: "Product 1", + affiliation: "Online Store", + coupon: "DISCOUNT10", + discount: 3.00, + item_brand: "Brand", + item_category: "Category", + price: 29.99, + quantity: 1 + }] + } +}); +``` + +## Korean E-commerce Considerations + +### Currency +```javascript +currency: "KRW", +value: 35000 // No decimals for KRW +``` + +### Common Korean Platform Integrations +- Cafe24: Uses custom dataLayer structure +- Shopify Korea: Standard GA4 format +- Naver SmartStore: Custom pixel implementation diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/form_tracking.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/form_tracking.md new file mode 100644 index 0000000..7d9d7d4 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/form_tracking.md @@ -0,0 +1,157 @@ +# Form Tracking Reference + +## GA4 Form Events + +### form_start +Fires on first interaction with form field. + +```javascript +dataLayer.push({ + event: "form_start", + form_id: "contact-form", + form_name: "Contact Us", + form_destination: "/submit-contact" +}); +``` + +### form_submit +Fires on successful form submission. + +```javascript +dataLayer.push({ + event: "form_submit", + form_id: "contact-form", + form_name: "Contact Us", + form_destination: "/submit-contact", + form_submit_text: "Send Message" +}); +``` + +### generate_lead +Fires when form generates a qualified lead. + +```javascript +dataLayer.push({ + event: "generate_lead", + currency: "USD", + value: 100, // Estimated lead value + form_id: "quote-request" +}); +``` + +## Form Field Events (Custom) + +### field_focus +```javascript +dataLayer.push({ + event: "field_focus", + form_id: "signup-form", + field_name: "email", + field_type: "email" +}); +``` + +### field_complete +```javascript +dataLayer.push({ + event: "field_complete", + form_id: "signup-form", + field_name: "email", + field_type: "email", + is_valid: true +}); +``` + +### field_error +```javascript +dataLayer.push({ + event: "field_error", + form_id: "signup-form", + field_name: "email", + error_message: "Invalid email format" +}); +``` + +## Form Abandonment Tracking + +### Detecting Abandonment +Track when user leaves form without submitting: + +```javascript +// Track form start +let formStarted = false; +document.querySelectorAll('form input, form select, form textarea') + .forEach(field => { + field.addEventListener('focus', function() { + if (!formStarted) { + formStarted = true; + dataLayer.push({ event: 'form_start', form_id: this.form.id }); + } + }); + }); + +// Track abandonment on page leave +window.addEventListener('beforeunload', function() { + if (formStarted && !formSubmitted) { + dataLayer.push({ + event: 'form_abandon', + form_id: 'contact-form', + last_field: lastFocusedField, + fields_completed: completedFieldCount + }); + } +}); +``` + +## GTM Trigger Configuration + +### Form Submission Trigger +| Setting | Value | +|---------|-------| +| Trigger Type | Form Submission | +| Wait for Tags | Check (if AJAX form) | +| Check Validation | Check | +| Form ID | equals `contact-form` | + +### Form Start Trigger (Custom Event) +| Setting | Value | +|---------|-------| +| Trigger Type | Custom Event | +| Event Name | form_start | +| Fire On | All Custom Events | + +## Common Form Types & Tracking + +### Contact Forms +Events: `form_start`, `form_submit`, `generate_lead` + +### Newsletter Signup +Events: `form_start`, `form_submit`, `sign_up` + +### Login Forms +Events: `form_start`, `login` + +### Search Forms +Events: `search` (with search_term parameter) + +### Multi-Step Forms +Track each step: +```javascript +dataLayer.push({ + event: "form_step", + form_id: "checkout-form", + step_number: 2, + step_name: "Shipping Address" +}); +``` + +## Validation Checklist + +- [ ] Form has id or name attribute +- [ ] All required fields have names +- [ ] Submit button identifiable +- [ ] form_start fires on first interaction +- [ ] form_submit fires only on success +- [ ] generate_lead has value parameter +- [ ] Error events track validation failures +- [ ] Abandonment tracking implemented (optional) diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ga4_events.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ga4_events.md new file mode 100644 index 0000000..e7a964f --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/ga4_events.md @@ -0,0 +1,177 @@ +# GA4 Recommended Events Reference + +## Automatically Collected Events +Events GA4 collects without configuration: +- `first_visit` - First time user visits +- `session_start` - Session begins +- `page_view` - Page loads (enhanced measurement) +- `scroll` - 90% scroll depth +- `click` - Outbound link clicks +- `file_download` - File download clicks +- `video_start`, `video_progress`, `video_complete` - YouTube embeds + +## E-commerce Events (Required Parameters) + +### view_item_list +```javascript +{ + event: "view_item_list", + ecommerce: { + item_list_id: "related_products", + item_list_name: "Related Products", + items: [{ + item_id: "SKU_12345", // required + item_name: "Product Name", // required + price: 29.99, + quantity: 1 + }] + } +} +``` + +### view_item +```javascript +{ + event: "view_item", + ecommerce: { + currency: "USD", + value: 29.99, + items: [{ + item_id: "SKU_12345", // required + item_name: "Product Name", // required + price: 29.99, + quantity: 1 + }] + } +} +``` + +### add_to_cart +```javascript +{ + event: "add_to_cart", + ecommerce: { + currency: "USD", + value: 29.99, + items: [{ + item_id: "SKU_12345", // required + item_name: "Product Name", // required + price: 29.99, + quantity: 1 + }] + } +} +``` + +### begin_checkout +```javascript +{ + event: "begin_checkout", + ecommerce: { + currency: "USD", + value: 99.99, + coupon: "SUMMER_SALE", + items: [...] + } +} +``` + +### add_payment_info +```javascript +{ + event: "add_payment_info", + ecommerce: { + currency: "USD", + value: 99.99, + payment_type: "credit_card", + items: [...] + } +} +``` + +### purchase +```javascript +{ + event: "purchase", + ecommerce: { + transaction_id: "T12345", // required, must be unique + value: 99.99, // required + currency: "USD", // required + tax: 4.99, + shipping: 5.99, + coupon: "SUMMER_SALE", + items: [{ + item_id: "SKU_12345", // required + item_name: "Product Name",// required + price: 29.99, + quantity: 2 + }] + } +} +``` + +## Lead Generation Events + +### generate_lead +```javascript +{ + event: "generate_lead", + currency: "USD", + value: 100 // estimated lead value +} +``` + +### sign_up +```javascript +{ + event: "sign_up", + method: "email" // or "google", "facebook", etc. +} +``` + +### login +```javascript +{ + event: "login", + method: "email" +} +``` + +## Engagement Events + +### search +```javascript +{ + event: "search", + search_term: "blue shoes" +} +``` + +### share +```javascript +{ + event: "share", + method: "twitter", + content_type: "article", + item_id: "article_123" +} +``` + +## Parameter Validation Rules + +| Parameter | Type | Max Length | Notes | +|-----------|------|------------|-------| +| event name | string | 40 chars | No spaces, alphanumeric + underscore | +| item_id | string | 100 chars | Required for e-commerce | +| item_name | string | 100 chars | Required for e-commerce | +| currency | string | 3 chars | ISO 4217 format (USD, KRW, etc.) | +| transaction_id | string | 100 chars | Must be unique per transaction | +| value | number | - | Numeric, no currency symbols | + +## Common Validation Errors + +1. **Missing required params**: `item_id` or `item_name` not in items array +2. **Wrong data type**: `value` as string instead of number +3. **Duplicate transaction_id**: Same ID used for multiple purchases +4. **Empty items array**: E-commerce event with no items +5. **Invalid currency**: Currency code not in ISO 4217 format diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/report_template.md b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/report_template.md new file mode 100644 index 0000000..3e187ef --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/docs/report_template.md @@ -0,0 +1,115 @@ +# GTM Audit Report Template + +## Executive Summary + +| Metric | Status | +|--------|--------| +| Container Installed | ✅ / ❌ | +| Container Valid | ✅ / ❌ | +| DataLayer Active | ✅ / ❌ | +| Tags Firing | X of Y | +| Critical Issues | X | +| Warnings | X | + +## Container Status + +**Container ID**: GTM-XXXXXX +**Installation Position**: head / body +**Multiple Containers**: Yes / No +**Noscript Fallback**: Present / Missing + +### Issues Found +- [ ] Issue description + +## DataLayer Analysis + +### Events Captured +| Event Name | Count | Has Issues | +|------------|-------|------------| +| page_view | 1 | No | +| add_to_cart | 0 | - | + +### DataLayer Quality +- [ ] Initialized before GTM +- [ ] Standard event naming +- [ ] Correct data types +- [ ] E-commerce structure valid + +## Tag Firing Report + +### Tags Fired ✅ +| Destination | Events | Parameters | +|-------------|--------|------------| +| GA4 | page_view | page_location, page_title | +| Meta Pixel | PageView | - | + +### Tags Not Detected ⚠️ +| Expected Tag | Reason | Priority | +|--------------|--------|----------| +| GA4 purchase | Event not triggered | High | + +## Network Request Analysis + +Total requests captured: X + +### By Destination +| Destination | Requests | Status | +|-------------|----------|--------| +| GA4 | X | ✅ | +| Meta | X | ✅ | +| Google Ads | 0 | ⚠️ | + +## Issues & Recommendations + +### Critical 🔴 +1. **Issue Title** + - Description + - Impact + - Recommended Fix + +### Warning 🟡 +1. **Issue Title** + - Description + - Recommended Fix + +### Info 🔵 +1. **Issue Title** + - Description + +## Action Items Checklist + +### Immediate (Critical) +- [ ] Action item 1 +- [ ] Action item 2 + +### Short-term (This Week) +- [ ] Action item 3 + +### Long-term (This Month) +- [ ] Action item 4 + +## Technical Details + +### Environment +- URL Audited: https://example.com +- Audit Timestamp: YYYY-MM-DD HH:MM:SS +- Browser: Chromium (headless) +- Viewport: 1920x1080 + +### Raw Data +Full JSON report available at: `gtm_audit_report.json` + +--- + +## Appendix: Tag Destination Reference + +| Tag Type | Network Pattern | +|----------|-----------------| +| GA4 | google-analytics.com/g/collect | +| UA (Legacy) | google-analytics.com/collect | +| Google Ads | googleads.g.doubleclick.net | +| Meta Pixel | facebook.com/tr | +| LinkedIn | px.ads.linkedin.com | +| TikTok | analytics.tiktok.com | +| Kakao | pixel.kakao.com | +| Naver | wcs.naver.com | diff --git a/ourdigital-custom-skills/13-ourdigital-gtm-manager/gtm_manager.py b/ourdigital-custom-skills/14-ourdigital-gtm-manager/gtm_manager.py similarity index 100% rename from ourdigital-custom-skills/13-ourdigital-gtm-manager/gtm_manager.py rename to ourdigital-custom-skills/14-ourdigital-gtm-manager/gtm_manager.py diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/requirements.txt b/ourdigital-custom-skills/14-ourdigital-gtm-manager/requirements.txt new file mode 100644 index 0000000..4777061 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/requirements.txt @@ -0,0 +1 @@ +playwright>=1.40.0 diff --git a/ourdigital-custom-skills/14-ourdigital-gtm-manager/setup.sh b/ourdigital-custom-skills/14-ourdigital-gtm-manager/setup.sh new file mode 100644 index 0000000..0168057 --- /dev/null +++ b/ourdigital-custom-skills/14-ourdigital-gtm-manager/setup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# GTM Audit Tool Setup Script + +echo "🔧 Setting up GTM Audit Tool..." + +# Check Python +if ! command -v python3 &> /dev/null; then + echo "❌ Python 3 is required but not installed." + exit 1 +fi + +echo "✅ Python 3 found" + +# Install dependencies +echo "📦 Installing Python dependencies..." +pip install -r requirements.txt + +# Install Playwright browsers +echo "🌐 Installing Playwright browsers..." +playwright install chromium + +echo "" +echo "✅ Setup complete!" +echo "" +echo "Usage:" +echo " python gtm_audit.py --url 'https://example.com' --journey full" +echo "" +echo "For Claude Code:" +echo " claude" +echo " > Run a GTM audit on https://example.com"