@technoapple/ga4
Version:
TypeScript Node.js library to support GA4 analytics.
386 lines (278 loc) • 10.6 kB
Markdown
# @technoapple/ga4
A lightweight TypeScript GA4 helper for browser apps.
It provides:
- A simple GA4 wrapper (`init`, `send`, direct `gtag` access)
- A `dataLayer` helper (`dataLayerHelper.get`)
- Opt-in automatic tracking plugins (event delegation, outbound tracking, SPA page views, impression tracking, URL cleanup, media query tracking)
## Why this library
GA4 and `gtag.js` are flexible, but many teams still need reusable browser-side utilities:
- Keep initialization and event sending consistent
- Add automatic tracking without wiring listeners repeatedly
- Keep plugin logic tree-shakeable and removable
## Installation
```bash
npm install @technoapple/ga4
```
## Quick Start
```ts
import { ga4 } from '@technoapple/ga4';
ga4.init({ targetId: 'G-XXXXXXX' });
ga4.send('sign_up', {
method: 'email',
plan: 'pro',
});
```
## Framework Examples
Use these integration guides for copy-paste setup patterns and lifecycle notes:
- [React example](docs/examples/react.md)
- [Vue example](docs/examples/vue.md)
- [Vanilla JavaScript example](docs/examples/vanilla.md)
## API
### `ga4.init(option)`
Initializes `window.dataLayer` and `window.gtag`, then sends:
- `gtag('js', new Date())`
- `gtag('config', option.targetId)`
```ts
ga4.init({ targetId: 'G-XXXXXXX' });
```
### `ga4.send(eventName, eventParameters)`
Sends a GA4 event through `gtag('event', ...)`.
```ts
ga4.send('purchase', {
transaction_id: 'order_123',
value: 99,
currency: 'USD',
});
```
### `ga4.gtag`
Direct access to the typed `gtag` function.
```ts
ga4.gtag('event', 'login', { method: 'google' });
```
### `ga4.use(PluginClass, options?)`
Registers a plugin and returns its instance.
```ts
import { ga4, OutboundLinkTracker } from '@technoapple/ga4';
const tracker = ga4.use(OutboundLinkTracker, {
eventName: 'outbound_link_click',
});
// Later
tracker.remove();
```
### `ga4.removeAll()`
Unregisters all plugins and calls each plugin's `remove()`.
### `dataLayerHelper.get(key, getLast?)`
Reads values from `window.dataLayer`.
- `getLast` omitted or `false`: returns first matching value
- `getLast` set to `true`: returns last matching value
```ts
import { dataLayerHelper } from '@technoapple/ga4';
const firstValue = dataLayerHelper.get('campaign');
const latestValue = dataLayerHelper.get('campaign', true);
```
## Plugin Catalog
All plugins implement:
```ts
interface GA4Plugin {
remove(): void;
}
```
### `EventTracker`
Declarative DOM tracking via attributes (default prefix: `data-ga4-`).
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `events` | `string[]` | `['click']` | DOM events to listen to via delegation |
| `attributePrefix` | `string` | `'data-ga4-'` | Reads attributes like `data-ga4-on` and `data-ga4-event-name` |
| `hitFilter` | `(params, element, event) => Record<string, unknown> \| null` | `undefined` | Return `null` to skip sending |
Defaults:
- `events: ['click']`
- `attributePrefix: 'data-ga4-'`
Example:
```html
<button
data-ga4-on="click"
data-ga4-event-name="video_play"
data-ga4-video-title="Summer Launch">
Play
</button>
```
```ts
import { ga4, EventTracker } from '@technoapple/ga4';
ga4.use(EventTracker);
```
### `OutboundLinkTracker`
Tracks clicks on links whose hostname differs from `location.hostname`.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `events` | `string[]` | `['click']` | Event types for delegated tracking |
| `linkSelector` | `string` | `'a, area'` | CSS selector for trackable links |
| `shouldTrackOutboundLink` | `(link, parseUrl) => boolean` | Built-in external-hostname matcher | Override outbound detection logic |
| `eventName` | `string` | `'outbound_link_click'` | Custom event name |
| `hitFilter` | `(params, element, event) => Record<string, unknown> \| null` | `undefined` | Return `null` to cancel event |
Defaults:
- `events: ['click']`
- `linkSelector: 'a, area'`
- `eventName: 'outbound_link_click'`
Default params:
- `link_url`
- `link_domain`
- `outbound: true`
### `OutboundFormTracker`
Tracks form submissions whose `form.action` points to an external hostname.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `formSelector` | `string` | `'form'` | CSS selector for forms to observe |
| `shouldTrackOutboundForm` | `(form, parseUrl) => boolean` | Built-in external-hostname matcher | Override outbound detection logic |
| `eventName` | `string` | `'outbound_form_submit'` | Custom event name |
| `hitFilter` | `(params, element, event) => Record<string, unknown> \| null` | `undefined` | Return `null` to cancel event |
Defaults:
- `formSelector: 'form'`
- `eventName: 'outbound_form_submit'`
Default params:
- `form_action`
- `form_domain`
- `outbound: true`
### `PageVisibilityTracker`
Tracks visible/hidden durations using `document.visibilityState`.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `sendInitialPageview` | `boolean` | `false` | Sends initial `page_view` when page is visible |
| `sessionTimeout` | `number` | `30` | Minutes before visible return triggers new `page_view` |
| `eventName` | `string` | `'page_visibility'` | Custom visibility event name |
| `hitFilter` | `(params) => Record<string, unknown> \| null` | `undefined` | Return `null` to skip event |
Defaults:
- `sendInitialPageview: false`
- `sessionTimeout: 30` (minutes)
- `eventName: 'page_visibility'`
Default params:
- `visibility_state`
- `visibility_duration`
- `page_path`
### `UrlChangeTracker`
Tracks SPA navigation by patching `history.pushState`, optional `replaceState`, and listening to `popstate`.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `shouldTrackUrlChange` | `(newPath, oldPath) => boolean` | `newPath !== oldPath` | Decide when to emit `page_view` |
| `trackReplaceState` | `boolean` | `false` | Also patch `history.replaceState` |
| `hitFilter` | `(params) => Record<string, unknown> \| null` | `undefined` | Return `null` to skip event |
Defaults:
- `trackReplaceState: false`
Sends `page_view` with:
- `page_path`
- `page_title`
- `page_location`
### `ImpressionTracker`
Tracks element impressions with `IntersectionObserver` and dynamic DOM changes via `MutationObserver`.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `elements` | `Array<string \| ImpressionElementConfig>` | `[]` | Element IDs or config objects to observe |
| `rootMargin` | `string` | `'0px'` | Passed to `IntersectionObserver` |
| `attributePrefix` | `string` | `'data-ga4-'` | Reads matching element attributes into params |
| `eventName` | `string` | `'element_impression'` | Custom event name |
| `hitFilter` | `(params, element) => Record<string, unknown> \| null` | `undefined` | Return `null` to skip event |
Defaults:
- `rootMargin: '0px'`
- `attributePrefix: 'data-ga4-'`
- `eventName: 'element_impression'`
Can observe with element IDs or configs:
```ts
import { ga4, ImpressionTracker } from '@technoapple/ga4';
ga4.use(ImpressionTracker, {
elements: [
'hero-banner',
{ id: 'cta-block', threshold: 0.5, trackFirstImpressionOnly: true },
],
});
```
### `CleanUrlTracker`
Intercepts `gtag` calls for `config` and `page_view` payloads and normalizes:
- `page_location`
- `page_path`
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `stripQuery` | `boolean` | `false` | Remove all query params unless allowlist is set |
| `queryParamsAllowlist` | `string[]` | `undefined` | Keep only selected params when stripping query |
| `queryParamsDenylist` | `string[]` | `undefined` | Remove selected params when not stripping query |
| `trailingSlash` | `'add' \| 'remove'` | `undefined` | Normalize `page_path` slash behavior |
| `urlFilter` | `(url) => string` | `undefined` | Final custom URL transform |
Options include:
- `stripQuery`
- `queryParamsAllowlist`
- `queryParamsDenylist`
- `trailingSlash: 'add' | 'remove'`
- `urlFilter`
### `MediaQueryTracker`
Tracks responsive breakpoint changes using `matchMedia`.
Options matrix:
| Option | Type | Default | Notes |
|---|---|---|---|
| `definitions` | `MediaQueryDefinition[]` | `[]` | Named breakpoint sets to track |
| `changeTemplate` | `(oldValue, newValue) => string` | `${oldValue} => ${newValue}` | Label formatter for change payload |
| `changeTimeout` | `number` | `1000` | Debounce delay in ms |
| `eventName` | `string` | `'media_query_change'` | Custom event name |
| `hitFilter` | `(params) => Record<string, unknown> \| null` | `undefined` | Return `null` to skip event |
Defaults:
- `changeTimeout: 1000`
- `eventName: 'media_query_change'`
Default params:
- `media_query_name`
- `media_query_value`
- `media_query_change`
## Plugin Lifecycle
Use either approach:
```ts
const plugin = ga4.use(EventTracker);
plugin.remove();
```
```ts
ga4.removeAll();
```
## Full Example
```ts
import {
ga4,
EventTracker,
OutboundLinkTracker,
UrlChangeTracker,
CleanUrlTracker,
} from '@technoapple/ga4';
ga4.init({ targetId: 'G-XXXXXXX' });
ga4.use(CleanUrlTracker, {
stripQuery: true,
queryParamsAllowlist: ['utm_source', 'utm_medium', 'utm_campaign'],
trailingSlash: 'remove',
});
ga4.use(EventTracker, { events: ['click', 'submit'] });
ga4.use(OutboundLinkTracker);
ga4.use(UrlChangeTracker, { trackReplaceState: true });
ga4.send('app_initialized', { env: 'production' });
```
## TypeScript Support
The package ships type definitions and exports plugin option types, including:
- `EventTrackerOptions`
- `OutboundLinkTrackerOptions`
- `OutboundFormTrackerOptions`
- `PageVisibilityTrackerOptions`
- `UrlChangeTrackerOptions`
- `ImpressionTrackerOptions`
- `CleanUrlTrackerOptions`
- `MediaQueryTrackerOptions`
## Development
```bash
npm run build
npm test
npm run test:coverage
```
## Notes
- Browser-focused library: APIs rely on `window`, `document`, and browser events.
- Call `ga4.init(...)` before sending events or registering plugins.
- If you register many plugins, clean them up with `remove()` or `ga4.removeAll()` to avoid duplicate listeners in long-lived apps.
## License
ISC