diff --git a/README.md b/README.md
index f80033c..398754e 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ If a visitor arrives at a website that uses the Nuxt UTM module and a UTM parame
- **📍 UTM Tracking**: Easily capture UTM parameters to gain insights into traffic sources and campaign performance.
- **🔍 Intelligent De-duplication**: Smart recognition of page refreshes to avoid data duplication, ensuring each visit is uniquely accounted for.
- **🔗 Comprehensive Data Collection**: Alongside UTM parameters, gather additional context such as referrer details, user agent, landing page url, browser language, and screen resolution. This enriched data empowers your marketing strategies with a deeper understanding of campaign impact.
+- **🔌 Hooks & Extensibility**: Three runtime hooks (`utm:before-track`, `utm:before-persist`, `utm:tracked`) let you skip tracking, enrich data with custom parameters, or trigger side effects after tracking completes.
## Quick Setup
@@ -86,6 +87,9 @@ const utm = useNuxtUTM()
// - enableTracking(): Enable UTM tracking
// - disableTracking(): Disable UTM tracking
// - clearData(): Clear all stored UTM data
+// - onBeforeTrack(cb): Hook called before data collection
+// - onBeforePersist(cb): Hook called to enrich/modify collected data before saving
+// - onTracked(cb): Hook called after data is saved
```
@@ -125,11 +129,7 @@ const rejectTracking = () => {
Privacy Settings
-
+
Enable UTM tracking
@@ -195,6 +195,9 @@ The `data` property contains an array of UTM parameters collected. Each element
"gclidParams": {
"gclid": "CjklsefawEFRfeafads",
"gad_source": "1"
+ },
+ "customParams": {
+ "fbclid": "abc123"
}
}
]
@@ -210,6 +213,149 @@ Each entry provides a `timestamp` indicating when the UTM parameters were collec
- **Data Clearing**: Ability to completely remove all collected data
- **Session Management**: Automatically manages sessions to avoid duplicate tracking
+### Hooks
+
+The module provides three runtime hooks that let you extend the tracking pipeline. You can use them to skip tracking, enrich data with custom parameters, or trigger side effects after tracking completes. Hooks can be registered via a Nuxt plugin or through the `useNuxtUTM` composable.
+
+This keeps your tracking strategy flexible: enrich once in your app, then forward the same enriched payload wherever you need it.
+
+#### Available Hooks
+
+| Hook | When it fires | Receives | Purpose |
+| ------------------ | -------------------------------------- | ----------------------------------------------- | ---------------------------------------------------- |
+| `utm:before-track` | Before data collection | `BeforeTrackContext` (`{ route, query, skip }`) | Conditionally skip tracking by setting `skip = true` |
+| `utm:before-persist` | After data is collected, before saving | `DataObject` (mutable) | Enrich or modify the data, add `customParams` |
+| `utm:tracked` | After data is saved to localStorage | `DataObject` (final) | Side effects: send to API, fire analytics, log |
+
+#### Registering Hooks via Plugin
+
+Create a Nuxt plugin to register hooks that run on every page visit:
+
+```typescript
+// plugins/utm-hooks.client.ts
+export default defineNuxtPlugin((nuxtApp) => {
+ // Skip tracking on admin pages
+ nuxtApp.hook('utm:before-track', (context) => {
+ if (context.route.path.startsWith('/admin')) {
+ context.skip = true
+ }
+ })
+
+ // Add custom marketing parameters
+ nuxtApp.hook('utm:before-persist', (data) => {
+ const query = nuxtApp._route.query
+ if (query.fbclid) {
+ data.customParams = {
+ ...data.customParams,
+ fbclid: String(query.fbclid),
+ }
+ }
+ if (query.msclkid) {
+ data.customParams = {
+ ...data.customParams,
+ msclkid: String(query.msclkid),
+ }
+ }
+ })
+
+ // Send data to your backend after tracking
+ nuxtApp.hook('utm:tracked', async (data) => {
+ await $fetch('/api/marketing/track', {
+ method: 'POST',
+ body: data,
+ })
+ })
+})
+```
+
+#### Registering Hooks via Composable
+
+The `useNuxtUTM` composable provides convenience methods for registering hooks. Each method returns a cleanup function to unregister the hook.
+
+```vue
+
+```
+
+#### Example: add `pageCategory`
+
+Use `utm:before-persist` to enrich every tracked event with a normalized `pageCategory`. This pattern is useful when you want one internal taxonomy that can be reused across your app and backend.
+
+```typescript
+// plugins/utm-page-category.client.ts
+export default defineNuxtPlugin((nuxtApp) => {
+ nuxtApp.hook('utm:before-persist', (data) => {
+ const url = new URL(data.additionalInfo.landingPageUrl)
+ const explicitCategory = url.searchParams.get('page_category')
+
+ // Optional fallback categorization from pathname
+ const fallbackCategory = url.pathname.startsWith('/pricing') ? 'pricing' : 'general'
+
+ data.customParams = {
+ ...data.customParams,
+ pageCategory: explicitCategory ?? fallbackCategory,
+ }
+ })
+})
+```
+
+Tracked data will include:
+
+```json
+{
+ "customParams": {
+ "pageCategory": "pricing"
+ }
+}
+```
+
+#### Hook: `utm:before-track`
+
+Called before any data collection begins. The handler receives a `BeforeTrackContext` object with `route`, `query`, and a `skip` flag. Set `skip = true` to prevent tracking for the current page visit.
+
+```typescript
+nuxtApp.hook('utm:before-track', (context) => {
+ // context.route - the current route object
+ // context.query - the current URL query parameters
+ // context.skip - set to true to skip tracking
+})
+```
+
+#### Hook: `utm:before-persist`
+
+Called after the `DataObject` is built but before it is checked for duplicates and saved. The handler receives the `DataObject` directly and can mutate it to add or modify fields. This is the primary hook for adding `customParams`.
+
+```typescript
+nuxtApp.hook('utm:before-persist', (data) => {
+ // Add any custom tracking parameters
+ data.customParams = {
+ ...data.customParams,
+ myCustomField: 'value',
+ }
+})
+```
+
+> Note: `customParams` are not included in the de-duplication check. Only UTM parameters, GCLID parameters, and session ID are compared.
+
+#### Hook: `utm:tracked`
+
+Called after data is saved to localStorage. The handler receives the final `DataObject`. Use this for side effects like sending data to a backend or triggering analytics events.
+
+```typescript
+nuxtApp.hook('utm:tracked', async (data) => {
+ console.log('Tracked:', data.utmParams, data.customParams)
+})
+```
+
## Development
```bash
diff --git a/eslint.config.mjs b/eslint.config.mjs
index ad56c89..4f44f47 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -17,6 +17,7 @@ export default createConfigForNuxt({
'@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
'@stylistic/semi': ['error', 'never'],
'@stylistic/comma-dangle': ['error', 'always-multiline'],
+ '@stylistic/arrow-parens': ['error', 'always'],
'@stylistic/operator-linebreak': 'off',
'@stylistic/brace-style': 'off',
'@stylistic/indent-binary-ops': 'off',
diff --git a/playground/app.vue b/playground/app.vue
index d4865da..4b1f42a 100644
--- a/playground/app.vue
+++ b/playground/app.vue
@@ -6,22 +6,18 @@
Tracking Controls
Tracking is currently:
-
+
{{ utm.trackingEnabled.value ? 'ENABLED' : 'DISABLED' }}
-
+
Enable Tracking
-
+
Disable Tracking
+ Custom Hook Testing
+
+
+ Track pageCategory: pricing
+
+
+ Track pageCategory: features
+
+
+
Try visiting with UTM parameters:
Add UTM params to URL
+
+ Then click a custom hook button above and check
+ customParams.pageCategory in collected data.
+
@@ -51,6 +61,15 @@
import { useNuxtUTM } from '#imports'
const utm = useNuxtUTM()
+
+const visitWithPageCategory = (pageCategory) => {
+ const url = new URL(window.location.href)
+ url.searchParams.set('utm_source', 'playground')
+ url.searchParams.set('utm_medium', 'manual-test')
+ url.searchParams.set('utm_campaign', 'custom-hook')
+ url.searchParams.set('page_category', pageCategory)
+ window.location.href = `${url.pathname}?${url.searchParams.toString()}`
+}