gtm-implementation

SKILL.md

GTM Implementation - DataLayer + GTM API

Implement complete GTM tracking by adding dataLayer events to your code AND creating GTM variables, triggers, and tags via API.

Core Mission

Transform analytics-ready DOM elements into fully tracked events with:

  1. Code Implementation: Add dataLayer.push() calls to React/Next.js/Vue components
  2. GTM Configuration: Create variables, triggers, and tags via GTM API

Workflow

Phase 0: Load Business Context (if available)

Check for gtm-context.md in the project root:

  • If found: read it silently and use the business context throughout this skill run
  • If not found: proceed normally - ask context questions as usual during the workflow

This file is created automatically by gtm-analytics-audit at the end of its first run.

Phase 1: Context & Prerequisites

Step 1.1: Load Tracking Plan

Check for gtm-tracking-plan.json (from gtm-strategy skill):
- If exists → Load events and parameters
- If missing → Ask user which events to implement OR suggest running gtm-strategy first

Check for gtm-config.json and gtm-token.json (from gtm-setup skill):
- If missing → Suggest running gtm-setup skill first
- If exists → Verify API connection

Step 1.2: Detect Framework

Check package.json:
- React version
- Next.js version and router type (App Router vs Pages Router)
- Vue version
- TypeScript vs JavaScript

This determines:
- Component file extensions (.tsx vs .jsx vs .vue)
- Import patterns
- Syntax for className vs class
- 'use client' directive for Next.js App Router

Step 1.3: Scan for Target Elements

Using Glob and Grep, find elements to instrument:

For CTA tracking:
- Search for: class=".*js-cta.*" or id="cta_.*"
- Search for: <button, <Link components

For form tracking:
- Search for: class=".*js-form.*" or id="form_.*"
- Search for: <form tags with onSubmit handlers

For navigation tracking:
- Search for: class=".*js-nav.*" or id="nav_.*"
- Search for: <Link, <a tags

Match found elements against events in tracking plan.

Step 1.4: Resolve Active Workspace (Dynamic)

Before any GTM API operation, always resolve the current workspace dynamically:

// List all workspaces for this container
const workspacesResponse = await tagmanager.accounts.containers.workspaces.list({
  parent: `accounts/${accountId}/containers/${containerId}`
})

const workspaces = workspacesResponse.data.workspace || []

if (workspaces.length === 0) {
  throw new Error('No workspaces found in this GTM container. Create one in the GTM UI first.')
}

// Always use the first available workspace
const workspace = workspaces[0]
const workspaceId = workspace.workspaceId

console.log(`✓ Active workspace: "${workspace.name}" (ID: ${workspaceId})`)

Why: GTM deletes a workspace after manual submission in the GTM UI and creates a new one with a new ID. The stored workspaceId in gtm-config.json becomes stale. Never assume a specific workspace ID exists.

Phase 2: DataLayer Implementation (Code Changes)

For each event in tracking plan, implement dataLayer.push() in actual code files.

Step 2.1: CTA Click Implementation

Find target (example):

// File: app/page.tsx
<button
  className="btn primary js-track js-cta js-click js-hero"
  id="cta_hero_get_started"
>
  Get Started
</button>

Implement tracking using Edit tool:

Before:

<button
  className="btn primary js-track js-cta js-click js-hero"
  id="cta_hero_get_started"
>
  Get Started
</button>

After:

<button
  className="btn primary js-track js-cta js-click js-hero"
  id="cta_hero_get_started"
  onClick={() => {
    if (typeof window !== 'undefined' && window.dataLayer) {
      window.dataLayer.push({
        event: 'cta_click',
        cta_location: 'hero',
        cta_type: 'primary',
        cta_text: 'Get Started',
        cta_destination: '/signup'
      })
    }
  }}
>
  Get Started
</button>

Framework-Specific Considerations:

Next.js App Router (requires 'use client'):

'use client'

import { useRouter } from 'next/navigation'

export default function HeroCTA() {
  const router = useRouter()

  return (
    <button
      className="btn primary js-track js-cta js-click js-hero"
      id="cta_hero_get_started"
      onClick={() => {
        // Track event
        if (typeof window !== 'undefined' && window.dataLayer) {
          window.dataLayer.push({
            event: 'cta_click',
            cta_location: 'hero',
            cta_type: 'primary',
            cta_text: 'Get Started',
            cta_destination: '/signup'
          })
        }
        // Navigate
        router.push('/signup')
      }}
    >
      Get Started
    </button>
  )
}

Step 2.2: Form Submit Implementation

Find target:

<form
  className="contact-form js-track js-form js-submit js-hero"
  id="form_hero_contact"
  onSubmit={handleSubmit}
>

Implement tracking:

Before:

<form
  className="contact-form js-track js-form js-submit js-hero"
  id="form_hero_contact"
  onSubmit={handleSubmit}
>

After:

<form
  className="contact-form js-track js-form js-submit js-hero"
  id="form_hero_contact"
  onSubmit={(e) => {
    // Track form submission
    if (typeof window !== 'undefined' && window.dataLayer) {
      window.dataLayer.push({
        event: 'form_submit',
        form_name: 'contact',
        form_location: 'hero',
        form_type: 'contact_request'
      })
    }
    // Call original handler
    handleSubmit(e)
  }}
>

Step 2.3: Navigation Click Implementation

Find target (Next.js Link):

<Link href="/pricing">Pricing</Link>

Implement tracking:

Before:

<Link
  href="/pricing"
  className="nav-link js-track js-nav js-click js-header"
  id="nav_header_pricing"
>
  Pricing
</Link>

After:

<Link
  href="/pricing"
  className="nav-link js-track js-nav js-click js-header"
  id="nav_header_pricing"
  onClick={() => {
    if (typeof window !== 'undefined' && window.dataLayer) {
      window.dataLayer.push({
        event: 'navigation_click',
        nav_location: 'header',
        nav_type: 'menu_link',
        nav_text: 'Pricing',
        nav_destination: '/pricing'
      })
    }
  }}
>
  Pricing
</Link>

Step 2.4: Parameter Extraction Logic

Extract parameters from DOM elements intelligently:

cta_location: From ID attribute

// id="cta_hero_get_started" → location: "hero"
const id = element.getAttribute('id') // "cta_hero_get_started"
const location = id.split('_')[1] // "hero"

cta_text: From button innerText

// <button>Get Started</button> → cta_text: "Get Started"
cta_text: 'Get Started'

cta_destination: From href or programmatic navigation

// <Link href="/signup"> → cta_destination: "/signup"
// onClick={() => router.push('/pricing')} → cta_destination: "/pricing"

cta_type: From CSS classes

// className includes "btn-primary" → type: "primary"
// className includes "btn-secondary" → type: "secondary"

Phase 3: GTM Container Configuration (API Calls)

Create variables, triggers, and tags via GTM API to process the dataLayer events.

Step 3.1: Create Data Layer Variables

For each parameter in each event, create a GTM variable:

Example: cta_click event needs 4 variables

// Variable 1: CTA Location
{
  name: "DLV - CTA Location",
  type: "v",
  parameter: [
    { type: "TEMPLATE", key: "name", value: "cta_location" },
    { type: "INTEGER", key: "dataLayerVersion", value: "2" }
  ]
}

// Variable 2: CTA Type
{
  name: "DLV - CTA Type",
  type: "v",
  parameter: [
    { type: "TEMPLATE", key: "name", value: "cta_type" },
    { type: "INTEGER", key: "dataLayerVersion", value: "2" }
  ]
}

// Variable 3: CTA Text
{
  name: "DLV - CTA Text",
  type: "v",
  parameter: [
    { type: "TEMPLATE", key: "name", value: "cta_text" },
    { type: "INTEGER", key: "dataLayerVersion", value: "2" }
  ]
}

// Variable 4: CTA Destination
{
  name: "DLV - CTA Destination",
  type: "v",
  parameter: [
    { type: "TEMPLATE", key: "name", value: "cta_destination" },
    { type: "INTEGER", key: "dataLayerVersion", value: "2" }
  ]
}

Use GTM API to create:

tagmanager.accounts.containers.workspaces.variables.create({
  parent: `accounts/${accountId}/containers/${containerId}/workspaces/${workspaceId}`,
  requestBody: variableConfig
})

Step 3.2: Create Custom Event Triggers

For each event, create a trigger that fires on that custom event:

Example: cta_click trigger

{
  name: "CE - CTA Click",
  type: "CUSTOM_EVENT",
  customEventFilter: [
    {
      type: "EQUALS",
      parameter: [
        { type: "TEMPLATE", key: "arg0", value: "{{_event}}" },
        { type: "TEMPLATE", key: "arg1", value: "cta_click" }
      ]
    }
  ]
}

Use GTM API to create:

tagmanager.accounts.containers.workspaces.triggers.create({
  parent: `accounts/${accountId}/containers/${containerId}/workspaces/${workspaceId}`,
  requestBody: triggerConfig
})

Step 3.3: Create GA4 Event Tags

For each event, create a GA4 event tag that fires on the trigger:

Example: CTA Click tag

{
  name: "GA4 - CTA Click",
  type: "gaawe", // GA4 Event tag type
  parameter: [
    {
      type: "TEMPLATE",
      key: "eventName",
      value: "cta_click"
    },
    {
      type: "LIST",
      key: "eventParameters",
      list: [
        {
          type: "MAP",
          map: [
            { type: "TEMPLATE", key: "name", value: "cta_location" },
            { type: "TEMPLATE", key: "value", value: "{{DLV - CTA Location}}" }
          ]
        },
        {
          type: "MAP",
          map: [
            { type: "TEMPLATE", key: "name", value: "cta_type" },
            { type: "TEMPLATE", key: "value", value: "{{DLV - CTA Type}}" }
          ]
        },
        {
          type: "MAP",
          map: [
            { type: "TEMPLATE", key: "name", value: "cta_text" },
            { type: "TEMPLATE", key: "value", value: "{{DLV - CTA Text}}" }
          ]
        },
        {
          type: "MAP",
          map: [
            { type: "TEMPLATE", key: "name", value: "cta_destination" },
            { type: "TEMPLATE", key: "value", value: "{{DLV - CTA Destination}}" }
          ]
        }
      ]
    },
    {
      type: "TAG_REFERENCE",
      key: "measurementId",
      value: "{{GA4 Configuration Tag}}" // Reference to GA4 config tag
    }
  ],
  firingTriggerId: ["{{CE - CTA Click trigger ID}}"]
}

Use GTM API to create:

tagmanager.accounts.containers.workspaces.tags.create({
  parent: `accounts/${accountId}/containers/${containerId}/workspaces/${workspaceId}`,
  requestBody: tagConfig
})

Step 3.4: Handle Incremental Updates

Check for existing variables/triggers/tags before creating:

// List existing variables
const existingVariables = await tagmanager.accounts.containers.workspaces.variables.list({
  parent: `accounts/${accountId}/containers/${containerId}/workspaces/${workspaceId}`
})

// Check if "DLV - CTA Location" already exists
const exists = existingVariables.data.variable?.find(v => v.name === "DLV - CTA Location")

if (exists) {
  // Update existing variable
  tagmanager.accounts.containers.workspaces.variables.update({
    path: exists.path,
    requestBody: variableConfig
  })
} else {
  // Create new variable
  tagmanager.accounts.containers.workspaces.variables.create({...})
}

Phase 4: Container Version Creation

After all variables/triggers/tags are created, create a new container version:

const version = await tagmanager.accounts.containers.workspaces.create_version({
  path: `accounts/${accountId}/containers/${containerId}/workspaces/${workspaceId}`,
  requestBody: {
    name: `GTM Automation - ${new Date().toISOString()}`,
    notes: `Automated implementation via gtm-implementation skill

Events implemented:
- cta_click (12 elements)
- form_submit (3 elements)
- navigation_click (8 elements)

Variables created: 12
Triggers created: 3
Tags created: 3`
  }
})

console.log(`✓ Container version created: ${version.data.containerVersionId}`)
console.log(`→ Preview in GTM: ${version.data.containerVersion.tagManagerUrl}`)

Phase 5: Summary Report

Generate comprehensive implementation summary:

=== GTM Implementation Complete ===

--- Code Changes ---
Files modified: 8

app/page.tsx:
  ✓ Added CTA click tracking (3 buttons)
  ✓ Added form submit tracking (1 form)

components/Navbar.tsx:
  ✓ Added navigation click tracking (5 links)

components/Footer.tsx:
  ✓ Added navigation click tracking (3 links)
  ✓ Added form submit tracking (1 newsletter form)

... (4 more files)

--- DataLayer Events Implemented ---
✓ cta_click (12 elements tracked)
✓ form_submit (3 elements tracked)
✓ navigation_click (8 elements tracked)

Total events: 23

--- GTM Container Configuration ---
Account: 1234567890
Container: GTM-ABC1234
Workspace: [dynamically resolved name] (ID: [resolved ID])

Created via API:
  ✓ 12 Data Layer Variables
  ✓ 3 Custom Event Triggers
  ✓ 3 GA4 Event Tags

Container version created: 42
Preview URL: https://tagmanager.google.com/#/versions/accounts/123/containers/456/versions/42

--- Next Steps ---
1. Test tracking in GTM Preview mode
   → Invoke gtm-testing skill for guided testing

2. Review container version in GTM:
   → Open GTM → Versions → Version 42
   → Check variables, triggers, tags

3. Publish when ready:
   → GTM → Submit → Publish

Ready to test tracking? Invoke gtm-testing skill.

Important Guidelines

Code Implementation Best Practices

1. Preserve Existing Functionality

  • NEVER remove existing onClick handlers
  • Call original handlers AFTER tracking
  • Use wrapper functions when needed

2. Type Safety (TypeScript)

// Add proper typing
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
  // Track
  window.dataLayer?.push({...})
  // Original handler
  originalHandler(e)
}}

3. Framework-Specific Patterns

Next.js App Router:

  • Add 'use client' directive to files with onClick
  • Use useRouter from 'next/navigation'

React:

  • Use functional components with hooks
  • Maintain existing state management

Vue:

  • Use @click syntax
  • Maintain reactivity

4. Avoid Redundant Tracking

  • If element already has dataLayer.push, UPDATE it (don't duplicate)
  • Check for existing tracking code before adding new

GTM API Best Practices

1. Naming Conventions

  • Variables: "DLV - {Parameter Name}" (DLV = Data Layer Variable)
  • Triggers: "CE - {Event Name}" (CE = Custom Event)
  • Tags: "GA4 - {Event Name}"

2. Error Handling

try {
  await tagmanager.accounts.containers.workspaces.variables.create({...})
} catch (error) {
  if (error.code === 409) {
    // Variable already exists - update instead
  } else if (error.code === 403) {
    // Permission denied
  } else {
    throw error
  }
}

3. Batch Operations

  • Create all variables first
  • Then create triggers (which may reference variables)
  • Finally create tags (which reference both)

4. Always Resolve Workspace Dynamically Never use a hardcoded or cached workspaceId. Before any operation, call:

const workspaces = await tagmanager.accounts.containers.workspaces.list({
  parent: `accounts/${accountId}/containers/${containerId}`
})
const workspaceId = workspaces.data.workspace[0].workspaceId

GTM deletes and recreates workspaces on each publish, so stored IDs become stale.

Parameter Extraction Intelligence

Be smart about extracting parameters:

From ID attributes:

// id="cta_hero_get_started"
const [category, location, ...action] = id.split('_')
// category: "cta", location: "hero", action: ["get", "started"]

From class names:

// className="btn btn-primary"
const isPrimary = className.includes('primary')
const type = isPrimary ? 'primary' : 'secondary'

From content:

// <button>Get Started</button>
const text = element.innerText // "Get Started"

From href/destination:

// <Link href="/pricing">
const destination = href // "/pricing"

// onClick={() => router.push('/signup')}
// Parse from handler → destination: "/signup"

Scripts

scripts/gtm-api-client.js

Core GTM API wrapper for creating variables/triggers/tags (extracted from gtm-setup-auto.js logic)

scripts/create-variables.js

Variable creation logic with duplicate detection

scripts/create-triggers.js

Trigger creation logic for custom events

scripts/create-tags.js

GA4 event tag creation with parameter mapping

scripts/implement-datalayer.js

Analyzes tracking plan and implements dataLayer.push() in code using Edit tool

References

references/datalayer-patterns.md

Framework-specific dataLayer implementation patterns:

  • Next.js App Router ('use client' directive)
  • Next.js Pages Router
  • React (hooks, class components)
  • Vue (composition API, options API)
  • Vanilla JavaScript

references/gtm-ui-guide.md

Manual GTM UI instructions as fallback if API fails

references/event-taxonomies.md

Common event naming patterns (object_action vs action_object)

Assets

assets/templates/saas.json

Pre-built GTM config for SaaS product tracking:

  • trial_start, feature_usage, upgrade_click
  • account_created, plan_selected

assets/templates/ecommerce.json

Pre-built GTM config for e-commerce tracking:

  • product_view, add_to_cart, checkout_start
  • purchase_complete, product_search

Supporting Files

  • template.md - Framework-specific dataLayer push code patterns and GTM API payload templates
  • examples/sample.md - Example implementation output showing before/after code changes and console output

assets/templates/lead-generation.json

Pre-built GTM config for lead-gen tracking:

  • form_start, form_submit, content_download
  • demo_request, newsletter_signup

Execution Checklist

  • Tracking plan loaded or events specified
  • GTM API credentials validated
  • Framework detected
  • Target elements identified
  • DataLayer events implemented in code
  • Variables created via GTM API
  • Triggers created via GTM API
  • Tags created via GTM API
  • Container version created
  • Implementation summary generated
  • Testing instructions provided

Common Questions

Q: What if I don't have a tracking plan? A: Run gtm-strategy skill first to create one, OR specify events manually and we'll implement them.

Q: Can I implement tracking without using the GTM API? A: Yes. We'll implement dataLayer events in code and provide manual GTM UI instructions.

Q: Will this overwrite existing GTM configuration? A: No. We check for existing variables/triggers/tags and update them. New configs are added, not replaced.

Q: Can I implement tracking for only specific events? A: Yes. Specify which events to implement (e.g., "only implement cta_click tracking").

Q: What if my framework isn't supported? A: The dataLayer.push pattern works in any JavaScript environment. We'll provide vanilla JS implementation.

Q: My workspace ID changed after publishing in GTM. Will the skill break? A: No. The skill always lists available workspaces via the API and uses the first one found, regardless of its ID. You never need to update gtm-config.json after publishing.

Weekly Installs
46
GitHub Stars
11
First Seen
Feb 18, 2026
Installed on
gemini-cli43
codex42
opencode42
cursor41
amp39
github-copilot39