Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Test

on:
pull_request:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- run: npm test
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

## Unreleased

- Google Ads conversions now send the order total as the value (was the tax) and the `AW-` tag is configured by the tracker, so `send_to` can attribute (#2).
- The snippet and tracker are gated on a non-empty Measurement ID; a store that only ticks "Enable" no longer loads `gtag/js?id=` (#3). Tracker calls are guarded so a page without the snippet never throws.
- `debug_mode` and `user_id` are passed in one config object; debug mode works for logged-in shoppers (#4).
- All money fields are numbers, unit `price`/`discount` are derived from line totals, one item shape across the funnel (`item_id` = product id, `sku`, `item_variant`), and page fields are no longer stuffed into `items[]` (#5).
- New events: `view_item_list`, `remove_from_cart`, `add_shipping_info`. New "Skip Test Orders" setting (#6). Consent Mode defaults and `add_payment_info` are not included: the platform has no payment-step event, and consent defaults need a CMP to be useful.
- Settings labels, help text and README say Google Ads and Next Commerce (#7).
- Review follow-ups: the tracker reaches the storefront through `window.parent` inside try/catch (an embedded storefront no longer throws on every event); every GA4 event sets `send_to`; checkout `value` is item revenue rather than the order grand total, and line prices exclude tax (`price_excl_tax`) since `tax` is reported separately; the Ads tag is configured by the tracker, which is the single place that normalises the id (bare digits, spaces, lower case) and validates the label shape; coupon is omitted when absent; lines without a product are dropped; item lists are truncated to GA4's 200-item limit.
- Added a Node test harness (`npm test`) and a CI workflow.
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
# Google Analytics 4

Google Analytics 4 app for 29 Next that integrates Google Analytics 4 into any storefront theme with [Storefront Event Tracking](https://developers.29next.com/docs/themes/event-tracking/). App also includes [Enhanced Ecommerce](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag) event tracking using the `gtag()` events.
Google Analytics 4 app for Next Commerce. Installs the Google tag on any storefront theme and sends GA4 ecommerce events through [Storefront Event Tracking](https://developers.nextcommerce.com/docs/storefront/event-tracking). Optionally sends a Google Ads conversion on completed orders.

Also includes Google Adwords Conversion tracking integration with `gtag()` conversion events.
## Settings

**Google Analytics 4**
* [Installs Google Analytics 4](https://support.google.com/analytics/answer/9304153) globally with a setting for your Measurement ID
| Setting | Notes |
|---|---|
| Enable Google Analytics | Nothing loads until a Measurement ID is also set. |
| Google Analytics Measurement ID | `G-XXXXXXXXXX`. |
| Enable Google Ads Conversion Tracking | The tracker configures the `AW-` tag at startup and sends a `conversion` event on `checkout_completed`. |
| Google Ads Conversion ID | `AW-123456789`; a bare `123456789` or lower-case paste is normalised. |
| Google Ads Conversion Label | From the Ads conversion action (letters, digits, `_`, `-`). |
| Enable Debug Mode | Sends `debug_mode` so events show in GA4 DebugView. |
| Skip Test Orders | Suppresses `purchase` and `conversion` for orders flagged `is_test`. |

## Events

**Google Ecommerce Events**
* [Product Detail Impressions](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag#view_item_details)
* [Add to Cart](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag#add_or_remove_an_item_from_a_shopping_cart)
* [Begin Checkout](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag#initiate_the_checkout_process)
* [Purchases](https://developers.google.com/analytics/devguides/collection/ga4/ecommerce?client_type=gtag#make_a_purchase_or_issue_a_refund)
| Storefront event | GA4 event |
|---|---|
| `product_category_viewed` | `view_item_list` |
| `product_viewed` | `view_item` |
| `product_added_to_cart` | `add_to_cart` |
| `product_removed_from_cart` | `remove_from_cart` |
| `checkout_started` | `begin_checkout` |
| `checkout_shipping_method_submitted` | `add_shipping_info` |
| `checkout_completed` | `purchase` (+ Ads `conversion` when enabled) |

`page_view` is sent by the Google tag itself on `config`.

**Google Adwords Conversion Tracking**
Items share identifiers across the funnel so GA4 item reports join: `item_id` is the product id, `sku` and `item_variant` identify the child product, `price` and `discount` are per unit, and all money fields are numbers. `value` on `begin_checkout`, `add_shipping_info` and `purchase` is item revenue (the sum of the lines' `price_excl_tax`; if a payload ever lacks that field the tax-inclusive total is used, so a store seeing values that include tax should check its payloads), as GA4 defines it; `shipping` and `tax` travel in their own parameters. The Ads `conversion` value is the order total the merchant was paid. Every GA4 event carries `send_to` for the configured Measurement ID so a second Google tag on the page does not receive it.

* [Google Tag for Google Ads Conversion Tracking](https://support.google.com/google-ads/answer/7548399?hl=en)
## Files

- `manifest.json` — settings schema, snippet location, event tracker mapping.
- `snippets/global-header.html` — loads gtag and configures the GA4 tag. Rendered only when a Measurement ID is set.
- `tracking.js` — the event tracker, which also validates the Ads settings and configures the `AW-` tag. Runs in the platform's tracker frame (a direct child of the storefront page) and calls `gtag` on the parent window; every parent access is wrapped so a page without the snippet, or an embedded storefront with a cross-origin parent, never throws.

See `tracking.js` for a complete detailed view of the implementation.
## Tests

For more information on individual tracking events, see the [Storefront Theme Event Tracking guide](https://developers.29next.com/docs/themes/event-tracking/).
```bash
npm test
```

`tests/tracking.test.js` runs `tracking.js` with the same globals the platform provides (`app`, `analytics`, `window.parent`) and asserts the payload of every mapped event, plus the escaping in the snippet. No dependencies; Node 22, the version CI runs.
23 changes: 15 additions & 8 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,40 +18,47 @@
"type": "text",
"label": "Google Analytics Measurement ID",
"default": "",
"help_text": "",
"help_text": "Your GA4 Measurement ID (G-XXXXXXXXXX). The app does nothing until this is set.",
"required": false,
"max_length": 250
},
{
"name": "google_adwords_conversion_enabled",
"type": "checkbox",
"label": "Enable Google Adwords Conversion Tracking",
"help_text": "",
"label": "Enable Google Ads Conversion Tracking",
"help_text": "Sends a conversion event to Google Ads when an order is completed.",
"default": false
},
{
"name": "google_adwords_conversion_id",
"type": "text",
"label": "Google Adwords Conversion ID",
"label": "Google Ads Conversion ID",
"default": "",
"required": false,
"help_text": "Required when Google Adwords Conversion Tracking is enabled.",
"help_text": "Including the AW- prefix, e.g. AW-123456789. Required when Google Ads Conversion Tracking is enabled.",
"max_length": 250
},
{
"name": "google_adwords_conversion_label",
"type": "text",
"label": "Google Adwords Conversion Label",
"label": "Google Ads Conversion Label",
"default": "",
"required": false,
"help_text": "Required when Google Adwords Conversion Tracking is enabled.",
"help_text": "Required when Google Ads Conversion Tracking is enabled.",
"max_length": 250
},
{
"name": "google_analytics_debug_mode",
"type": "checkbox",
"label": "Enable Debug Mode",
"help_text": "Debug mode allows you to monitor transactions in real time.",
"help_text": "Sends debug_mode so events appear in GA4 DebugView in real time.",
"default": false
},
{
"name": "google_analytics_skip_test_orders",
"type": "checkbox",
"label": "Skip Test Orders",
"help_text": "Do not send purchase or conversion events for test orders.",
"default": false
}
]
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "google-analytics-4",
"private": true,
"description": "Google Analytics 4 app for Next Commerce storefronts",
"scripts": {
"test": "node --test"
}
}
16 changes: 9 additions & 7 deletions snippets/global-header.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
{% if app.settings.google_analytics_enabled %}
{% if app.settings.google_analytics_enabled and app.settings.google_analytics_measurement_id.strip %}
Comment thread
next-devin marked this conversation as resolved.

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ app.settings.google_analytics_measurement_id }}"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id={{ app.settings.google_analytics_measurement_id.strip|urlencode }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());

gtag('config', '{{ app.settings.google_analytics_measurement_id }}',
{% if user.is_authenticated %} { 'user_id': '{{ user.pk }}' }, {% endif %}
{% if app.settings.google_analytics_debug_mode %} { 'debug_mode': true }, {% endif %}
);
gtag('config', '{{ app.settings.google_analytics_measurement_id.strip|escapejs }}', {
{% if user.is_authenticated %}'user_id': '{{ user.pk|escapejs }}',{% endif %}
{% if app.settings.google_analytics_debug_mode %}'debug_mode': true,{% endif %}
});

{# The Google Ads tag is configured by tracking.js, which validates and normalises the id and label in one place. #}
</script>

{% endif %}
{% endif %}
Loading
Loading