# Google Tag Manager (/docs/integrations/gtm)



Improve can mirror every analytic event onto the **Google Tag Manager dataLayer**. That lets your marketing team fire GTM tags and **Google Ads conversions** on Improve events: all from GTM, without extra engineering per event.

<Callout type="warn">
  The mirrored `improve` block no longer carries the test or variant: only an
  anonymous `visitorId`. Variant attribution now happens **server-side inside
  Improve** (by joining events against the visitor's exposures), so you compare
  variants in the test **Monitor**, not by splitting GTM/Google Ads conversions
  on a dataLayer variable.
</Callout>

<Callout type="info">
  This is provided by the Improve SDK and is **on by default**. It requires an
  SDK version with dataLayer support. See [opting out](#turning-it-off) to
  disable it.
</Callout>

## How it works [#how-it-works]

Whenever the SDK records an event (a page view, a click, a conversion…), it also pushes an entry like this onto `window.dataLayer`:

```js
window.dataLayer.push({
	event: 'purchase',
	scope: 'checkout', // the required scope you passed to postAnalytic
	improve: {
		visitorId: 'visi_…', // anonymous visitor id
	},
	// value / currency and any params you pass to postAnalytic ride along too:
	value: 72.05,
	currency: 'USD',
	ecommerce: { transaction_id: 'T_1', items: [/* … */] },
	_improve: true, // marks the entry as coming from Improve
})
```

Because the `improve` block rides along on **every** event, any GTM tag you trigger on an Improve event can report it to Google Ads, GA4, or anywhere else. `scope`, `value`, `currency` and any `params` you pass to [`postAnalytic`](/docs/sdk/javascript#postanalytic) are spread onto the entry — `scope` is a stable identifier (not display text), so it's also a reliable GTM trigger condition when you need to fire a tag for one specific `event` + `scope` combination rather than every firing of that event name. The test and variant are **not** on the entry: attribution to a variant is done inside Improve from the visitor's exposures, so use the test Monitor to compare variants.

<Callout type="info">
  When you pass a GA4 `ecommerce` object in `params`, the SDK pushes
  `{ ecommerce: null }` first to clear any previous ecommerce object (the
  GTM/GA4 recommended reset that stops stale `items`/`coupon` values bleeding
  between events).
</Callout>

## Before you start [#before-you-start]

* A **Google Tag Manager** container installed on your site. If you host on our platform you can set `NEXT_PUBLIC_GTM_ID` and we'll load the container for you; otherwise add the standard GTM snippet.
* The **Improve SDK** running on the same pages (this is what fills the dataLayer).

## Setting up [#setting-up]

1. **Confirm events are flowing.** In GTM, open **Preview** and trigger an action on your site. You should see your Improve event (e.g. `purchase`) appear in the dataLayer with an `improve` object.

2. **(optional) Create Data Layer Variables for the values you pass.** In GTM, go to **Variables → New → Data Layer Variable** for fields like `value`, `currency`, or an `ecommerce` object so your tags can read them. (`improve.visitorId` is available too; the test and variant are not on the event.)

3. **Create a trigger for your event.** **Triggers → New → Custom Event** and set the event name to your Improve event (for example `purchase`). This fires whenever that event hits the dataLayer.

4. **Fire your tag.** Attach the trigger to your tag (for example a **Google Ads Conversion** tag) and pass through the value/currency variables you need.

5. **(Google Ads) create the conversion action** in Google Ads and connect it to the tag. To compare performance across variants, use the Improve test **Monitor**: variant attribution lives there, not in Google Ads.

## Turning it off [#turning-it-off]

The dataLayer push is enabled by default. To disable it, pass `dataLayer: false` when you set up the SDK:

```ts
const improve = new ImproveClientSDK({
	organizationId: '…',
	environment: 'production',
	dataLayer: false, // do not push events to window.dataLayer
})
```

The React and Next.js SDKs accept the same option in their setup args.

## Good to know [#good-to-know]

* **No loops.** Improve tags its own entries with `_improve: true`, so any importer (including Improve's own) knows to skip them.
* **GTM's own events are untouched.** Improve only adds its own entries; internal `gtm.*` events are left alone.
* **Works without GTM, too.** If GTM isn't on the page, `window.dataLayer` is just a harmless array; nothing breaks, the entries simply wait for a consumer.
