@stackonward/google-tag-manager-nuxt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/module.d.mts +22 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +103 -0
- package/dist/runtime/google-tag-manager-lifecycle.d.ts +21 -0
- package/dist/runtime/google-tag-manager-lifecycle.js +62 -0
- package/dist/runtime/plugins/google-tag-manager.client.d.ts +2 -0
- package/dist/runtime/plugins/google-tag-manager.client.js +34 -0
- package/dist/runtime/types.d.ts +36 -0
- package/dist/runtime/types.js +0 -0
- package/dist/transport.d.mts +30 -0
- package/dist/transport.d.ts +30 -0
- package/dist/transport.mjs +97 -0
- package/dist/types.d.mts +9 -0
- package/package.json +70 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @stackonward/google-tag-manager-nuxt
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add a provider-neutral analytics pipeline, Nuxt lifecycle integration, consent-aware Google Tag Manager transport, configurable global capture, and typed product event APIs. Distribute the integration through the Nuxt template and remove the superseded source-owned analytics runtime from the site shell.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @stackonward/analytics-core@0.1.0
|
|
13
|
+
- @stackonward/analytics-nuxt@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present yhy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @stackonward/google-tag-manager-nuxt
|
|
2
|
+
|
|
3
|
+
Consent-aware Google Tag Manager composition module for Nuxt. It installs the provider-neutral analytics runtime, configurable global capture and the Google transport.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @stackonward/google-tag-manager-nuxt @nuxt/scripts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
export default defineNuxtConfig({
|
|
13
|
+
modules: ["@stackonward/google-tag-manager-nuxt"],
|
|
14
|
+
googleTagManager: {
|
|
15
|
+
id: process.env.NUXT_PUBLIC_GTM_ID || "",
|
|
16
|
+
analytics: {
|
|
17
|
+
applicationId: "product-a",
|
|
18
|
+
environment: process.env.NUXT_PUBLIC_SITE_ENV || "development",
|
|
19
|
+
autoCapture: {
|
|
20
|
+
internalDomains: ["product.example"],
|
|
21
|
+
modulePrefix: "product_",
|
|
22
|
+
moduleRules: [{ selector: "header", module: "navigation" }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The integration starts disabled when the container ID is empty. Its default consent state denies every Google storage category. The product or CMP restores the user's decision and calls `useAnalyticsConsent().grant()` or `.deny()`.
|
|
30
|
+
|
|
31
|
+
The remote GTM container must contain and publish the required GA4 tags, triggers and consent checks. This package does not mutate Google account configuration.
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
|
|
35
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
import { GoogleTagManagerNuxtRuntimeConfig, GoogleTagManagerNuxtModuleOptions } from '../dist/runtime/types.js';
|
|
3
|
+
export { GoogleConsentState, GoogleConsentValue, GoogleTagManagerNuxtModuleOptions, GoogleTagManagerNuxtRuntimeConfig, GoogleTagManagerScriptOptions } from '../dist/runtime/types.js';
|
|
4
|
+
import { AnalyticsNuxtRuntimeConfig } from '@stackonward/analytics-nuxt';
|
|
5
|
+
|
|
6
|
+
interface NormalizedGoogleTagManagerOptions {
|
|
7
|
+
readonly runtime: GoogleTagManagerNuxtRuntimeConfig;
|
|
8
|
+
readonly analytics: AnalyticsNuxtRuntimeConfig;
|
|
9
|
+
}
|
|
10
|
+
declare function normalizeGoogleTagManagerNuxtOptions(input?: unknown): NormalizedGoogleTagManagerOptions;
|
|
11
|
+
|
|
12
|
+
declare module "@nuxt/schema" {
|
|
13
|
+
interface NuxtConfig {
|
|
14
|
+
googleTagManager?: GoogleTagManagerNuxtModuleOptions;
|
|
15
|
+
}
|
|
16
|
+
interface NuxtOptions {
|
|
17
|
+
googleTagManager?: GoogleTagManagerNuxtModuleOptions;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
declare const module$1: NuxtModule<GoogleTagManagerNuxtModuleOptions>;
|
|
21
|
+
|
|
22
|
+
export { module$1 as default, normalizeGoogleTagManagerNuxtOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
import { GoogleTagManagerNuxtRuntimeConfig, GoogleTagManagerNuxtModuleOptions } from '../dist/runtime/types.js';
|
|
3
|
+
export { GoogleConsentState, GoogleConsentValue, GoogleTagManagerNuxtModuleOptions, GoogleTagManagerNuxtRuntimeConfig, GoogleTagManagerScriptOptions } from '../dist/runtime/types.js';
|
|
4
|
+
import { AnalyticsNuxtRuntimeConfig } from '@stackonward/analytics-nuxt';
|
|
5
|
+
|
|
6
|
+
interface NormalizedGoogleTagManagerOptions {
|
|
7
|
+
readonly runtime: GoogleTagManagerNuxtRuntimeConfig;
|
|
8
|
+
readonly analytics: AnalyticsNuxtRuntimeConfig;
|
|
9
|
+
}
|
|
10
|
+
declare function normalizeGoogleTagManagerNuxtOptions(input?: unknown): NormalizedGoogleTagManagerOptions;
|
|
11
|
+
|
|
12
|
+
declare module "@nuxt/schema" {
|
|
13
|
+
interface NuxtConfig {
|
|
14
|
+
googleTagManager?: GoogleTagManagerNuxtModuleOptions;
|
|
15
|
+
}
|
|
16
|
+
interface NuxtOptions {
|
|
17
|
+
googleTagManager?: GoogleTagManagerNuxtModuleOptions;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
declare const module$1: NuxtModule<GoogleTagManagerNuxtModuleOptions>;
|
|
21
|
+
|
|
22
|
+
export { module$1 as default, normalizeGoogleTagManagerNuxtOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import analyticsNuxtModule, { normalizeAnalyticsNuxtOptions } from '@stackonward/analytics-nuxt';
|
|
2
|
+
import { defineNuxtModule, installModule, createResolver, addPlugin } from '@nuxt/kit';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
const consentValueSchema = z.enum(["granted", "denied"]);
|
|
6
|
+
const consentSchema = z.object({
|
|
7
|
+
ad_storage: consentValueSchema.optional(),
|
|
8
|
+
ad_user_data: consentValueSchema.optional(),
|
|
9
|
+
ad_personalization: consentValueSchema.optional(),
|
|
10
|
+
analytics_storage: consentValueSchema.optional(),
|
|
11
|
+
functionality_storage: consentValueSchema.optional(),
|
|
12
|
+
personalization_storage: consentValueSchema.optional(),
|
|
13
|
+
security_storage: consentValueSchema.optional(),
|
|
14
|
+
region: z.array(z.string().trim().min(2).max(6)).max(100).optional(),
|
|
15
|
+
wait_for_update: z.number().int().min(0).max(1e4).optional()
|
|
16
|
+
}).strict();
|
|
17
|
+
const optionsSchema = z.object({
|
|
18
|
+
id: z.string().trim().optional(),
|
|
19
|
+
enabled: z.boolean().optional(),
|
|
20
|
+
dataLayerName: z.string().trim().regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/).optional(),
|
|
21
|
+
defaultConsent: consentSchema.optional(),
|
|
22
|
+
grantedConsent: consentSchema.optional(),
|
|
23
|
+
script: z.object({
|
|
24
|
+
bundle: z.union([z.boolean(), z.literal("force")]).optional(),
|
|
25
|
+
proxy: z.boolean().optional(),
|
|
26
|
+
partytown: z.boolean().optional(),
|
|
27
|
+
warmupStrategy: z.union([
|
|
28
|
+
z.literal(false),
|
|
29
|
+
z.literal("preload"),
|
|
30
|
+
z.literal("preconnect"),
|
|
31
|
+
z.literal("dns-prefetch")
|
|
32
|
+
]).optional()
|
|
33
|
+
}).strict().optional(),
|
|
34
|
+
analytics: z.unknown().optional()
|
|
35
|
+
}).strict();
|
|
36
|
+
const DEFAULT_GOOGLE_CONSENT = Object.freeze({
|
|
37
|
+
ad_storage: "denied",
|
|
38
|
+
ad_user_data: "denied",
|
|
39
|
+
ad_personalization: "denied",
|
|
40
|
+
analytics_storage: "denied",
|
|
41
|
+
functionality_storage: "denied",
|
|
42
|
+
personalization_storage: "denied",
|
|
43
|
+
security_storage: "denied"
|
|
44
|
+
});
|
|
45
|
+
const DEFAULT_GRANTED_GOOGLE_CONSENT = Object.freeze({
|
|
46
|
+
...DEFAULT_GOOGLE_CONSENT,
|
|
47
|
+
analytics_storage: "granted",
|
|
48
|
+
security_storage: "granted"
|
|
49
|
+
});
|
|
50
|
+
function normalizeGoogleTagManagerNuxtOptions(input = {}) {
|
|
51
|
+
const parsed = optionsSchema.parse(input);
|
|
52
|
+
const id = parsed.id ?? "";
|
|
53
|
+
const analytics = normalizeAnalyticsNuxtOptions(parsed.analytics ?? {});
|
|
54
|
+
const configuredEnabled = parsed.enabled ?? id.length > 0;
|
|
55
|
+
const enabled = configuredEnabled && analytics.enabled;
|
|
56
|
+
if (id && !/^GTM-[A-Z0-9]+$/.test(id)) {
|
|
57
|
+
throw new Error("Google Tag Manager ID must match GTM-[A-Z0-9]+");
|
|
58
|
+
}
|
|
59
|
+
if (configuredEnabled && !id) {
|
|
60
|
+
throw new Error("Google Tag Manager ID is required when the integration is enabled");
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
runtime: Object.freeze({
|
|
64
|
+
id,
|
|
65
|
+
enabled,
|
|
66
|
+
dataLayerName: parsed.dataLayerName ?? "dataLayer",
|
|
67
|
+
defaultConsent: Object.freeze({ ...DEFAULT_GOOGLE_CONSENT, ...parsed.defaultConsent }),
|
|
68
|
+
grantedConsent: Object.freeze({
|
|
69
|
+
...DEFAULT_GRANTED_GOOGLE_CONSENT,
|
|
70
|
+
...parsed.grantedConsent
|
|
71
|
+
}),
|
|
72
|
+
script: Object.freeze({ ...parsed.script })
|
|
73
|
+
}),
|
|
74
|
+
analytics
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const module$1 = defineNuxtModule({
|
|
79
|
+
meta: {
|
|
80
|
+
name: "@stackonward/google-tag-manager-nuxt",
|
|
81
|
+
configKey: "googleTagManager",
|
|
82
|
+
compatibility: { nuxt: ">=3.15.0" }
|
|
83
|
+
},
|
|
84
|
+
async setup(options, nuxt) {
|
|
85
|
+
const normalized = normalizeGoogleTagManagerNuxtOptions(options);
|
|
86
|
+
await installModule("@nuxt/scripts", {}, nuxt);
|
|
87
|
+
await installModule(
|
|
88
|
+
analyticsNuxtModule,
|
|
89
|
+
{
|
|
90
|
+
...normalized.analytics,
|
|
91
|
+
enabled: normalized.runtime.enabled && normalized.analytics?.enabled !== false
|
|
92
|
+
},
|
|
93
|
+
nuxt
|
|
94
|
+
);
|
|
95
|
+
nuxt.options.runtimeConfig.public.stackonwardGoogleTagManager = normalized.runtime;
|
|
96
|
+
if (normalized.runtime.enabled) {
|
|
97
|
+
const { resolve } = createResolver(import.meta.url);
|
|
98
|
+
addPlugin(resolve("./runtime/plugins/google-tag-manager.client"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export { module$1 as default, normalizeGoogleTagManagerNuxtOptions };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnalyticsNuxtLifecycleParticipant } from "@stackonward/analytics-nuxt";
|
|
2
|
+
import type { GoogleConsentState } from "./types.js";
|
|
3
|
+
export type GoogleTagManagerLifecycleErrorCode = "consent-api-unavailable" | "disposed";
|
|
4
|
+
export declare class GoogleTagManagerLifecycleError extends Error {
|
|
5
|
+
readonly code: GoogleTagManagerLifecycleErrorCode;
|
|
6
|
+
constructor(code: GoogleTagManagerLifecycleErrorCode);
|
|
7
|
+
}
|
|
8
|
+
export interface GoogleTagManagerScriptLifecycle {
|
|
9
|
+
readonly consent?: {
|
|
10
|
+
update(state: GoogleConsentState): void;
|
|
11
|
+
};
|
|
12
|
+
load(): Promise<unknown>;
|
|
13
|
+
reload(): Promise<unknown>;
|
|
14
|
+
remove(): boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface GoogleTagManagerLifecycleOptions {
|
|
17
|
+
readonly script: GoogleTagManagerScriptLifecycle;
|
|
18
|
+
readonly defaultConsent: GoogleConsentState;
|
|
19
|
+
readonly grantedConsent: GoogleConsentState;
|
|
20
|
+
}
|
|
21
|
+
export declare function createGoogleTagManagerLifecycleParticipant(options: GoogleTagManagerLifecycleOptions): AnalyticsNuxtLifecycleParticipant;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class GoogleTagManagerLifecycleError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code) {
|
|
4
|
+
super(`Google Tag Manager lifecycle error: ${code}`);
|
|
5
|
+
this.name = "GoogleTagManagerLifecycleError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function createGoogleTagManagerLifecycleParticipant(options) {
|
|
10
|
+
let attemptedLoad = false;
|
|
11
|
+
let loaded = false;
|
|
12
|
+
let disposed = false;
|
|
13
|
+
let startPromise;
|
|
14
|
+
const updateConsent = (state) => {
|
|
15
|
+
if (!options.script.consent) {
|
|
16
|
+
throw new GoogleTagManagerLifecycleError("consent-api-unavailable");
|
|
17
|
+
}
|
|
18
|
+
options.script.consent.update(state);
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
id: "google-tag-manager",
|
|
22
|
+
start() {
|
|
23
|
+
if (disposed) {
|
|
24
|
+
throw new GoogleTagManagerLifecycleError("disposed");
|
|
25
|
+
}
|
|
26
|
+
updateConsent(options.grantedConsent);
|
|
27
|
+
if (loaded) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (startPromise) {
|
|
31
|
+
return startPromise;
|
|
32
|
+
}
|
|
33
|
+
const operation = (async () => {
|
|
34
|
+
const load = attemptedLoad ? options.script.reload : options.script.load;
|
|
35
|
+
attemptedLoad = true;
|
|
36
|
+
await load.call(options.script);
|
|
37
|
+
loaded = true;
|
|
38
|
+
})();
|
|
39
|
+
const tracked = operation.finally(() => {
|
|
40
|
+
if (startPromise === tracked) {
|
|
41
|
+
startPromise = void 0;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
startPromise = tracked;
|
|
45
|
+
return tracked;
|
|
46
|
+
},
|
|
47
|
+
stop() {
|
|
48
|
+
if (disposed) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
updateConsent(options.defaultConsent);
|
|
52
|
+
},
|
|
53
|
+
dispose() {
|
|
54
|
+
if (disposed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
updateConsent(options.defaultConsent);
|
|
58
|
+
disposed = true;
|
|
59
|
+
options.script.remove();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useNuxtApp, useRuntimeConfig } from "nuxt/app";
|
|
2
|
+
import { createGoogleTagManagerTransport } from "../../google-tag-manager-transport";
|
|
3
|
+
import { createGoogleTagManagerLifecycleParticipant } from "../google-tag-manager-lifecycle.js";
|
|
4
|
+
export default defineNuxtPlugin({
|
|
5
|
+
name: "stackonward:google-tag-manager",
|
|
6
|
+
dependsOn: ["stackonward:analytics"],
|
|
7
|
+
setup() {
|
|
8
|
+
const config = useRuntimeConfig().public.stackonwardGoogleTagManager;
|
|
9
|
+
const app = useNuxtApp();
|
|
10
|
+
const analytics = app.$analyticsRuntime;
|
|
11
|
+
if (!analytics) {
|
|
12
|
+
throw new Error("Google Tag Manager requires @stackonward/analytics-nuxt");
|
|
13
|
+
}
|
|
14
|
+
const script = useScriptGoogleTagManager({
|
|
15
|
+
id: config.id,
|
|
16
|
+
l: config.dataLayerName,
|
|
17
|
+
defaultConsent: config.defaultConsent,
|
|
18
|
+
trigger: "manual",
|
|
19
|
+
...config.script
|
|
20
|
+
});
|
|
21
|
+
analytics.registerLifecycleParticipant(
|
|
22
|
+
createGoogleTagManagerLifecycleParticipant({
|
|
23
|
+
script,
|
|
24
|
+
defaultConsent: config.defaultConsent,
|
|
25
|
+
grantedConsent: config.grantedConsent
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
analytics.registerTransport(
|
|
29
|
+
createGoogleTagManagerTransport({
|
|
30
|
+
push: (event) => script.proxy.dataLayer.push(event)
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AnalyticsNuxtModuleOptions } from "@stackonward/analytics-nuxt";
|
|
2
|
+
export type GoogleConsentValue = "granted" | "denied";
|
|
3
|
+
export interface GoogleConsentState {
|
|
4
|
+
readonly ad_storage?: GoogleConsentValue;
|
|
5
|
+
readonly ad_user_data?: GoogleConsentValue;
|
|
6
|
+
readonly ad_personalization?: GoogleConsentValue;
|
|
7
|
+
readonly analytics_storage?: GoogleConsentValue;
|
|
8
|
+
readonly functionality_storage?: GoogleConsentValue;
|
|
9
|
+
readonly personalization_storage?: GoogleConsentValue;
|
|
10
|
+
readonly security_storage?: GoogleConsentValue;
|
|
11
|
+
readonly region?: readonly string[];
|
|
12
|
+
readonly wait_for_update?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GoogleTagManagerScriptOptions {
|
|
15
|
+
readonly bundle?: boolean | "force";
|
|
16
|
+
readonly proxy?: boolean;
|
|
17
|
+
readonly partytown?: boolean;
|
|
18
|
+
readonly warmupStrategy?: false | "preload" | "preconnect" | "dns-prefetch";
|
|
19
|
+
}
|
|
20
|
+
export interface GoogleTagManagerNuxtModuleOptions {
|
|
21
|
+
readonly id?: string;
|
|
22
|
+
readonly enabled?: boolean;
|
|
23
|
+
readonly dataLayerName?: string;
|
|
24
|
+
readonly defaultConsent?: GoogleConsentState;
|
|
25
|
+
readonly grantedConsent?: GoogleConsentState;
|
|
26
|
+
readonly script?: GoogleTagManagerScriptOptions;
|
|
27
|
+
readonly analytics?: AnalyticsNuxtModuleOptions;
|
|
28
|
+
}
|
|
29
|
+
export interface GoogleTagManagerNuxtRuntimeConfig {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly enabled: boolean;
|
|
32
|
+
readonly dataLayerName: string;
|
|
33
|
+
readonly defaultConsent: GoogleConsentState;
|
|
34
|
+
readonly grantedConsent: GoogleConsentState;
|
|
35
|
+
readonly script: GoogleTagManagerScriptOptions;
|
|
36
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as _stackonward_analytics_core from '@stackonward/analytics-core';
|
|
2
|
+
import { AnalyticsPrimitive, AnalyticsEnvelope, AnalyticsValidationIssue, AnalyticsTransport } from '@stackonward/analytics-core';
|
|
3
|
+
|
|
4
|
+
type GoogleTagManagerDataLayerEvent = Readonly<Record<string, AnalyticsPrimitive> & {
|
|
5
|
+
event: string;
|
|
6
|
+
}>;
|
|
7
|
+
type GoogleEventProjectionResult = {
|
|
8
|
+
readonly success: true;
|
|
9
|
+
readonly data: GoogleTagManagerDataLayerEvent;
|
|
10
|
+
} | {
|
|
11
|
+
readonly success: false;
|
|
12
|
+
readonly issues: readonly AnalyticsValidationIssue[];
|
|
13
|
+
};
|
|
14
|
+
declare function projectGoogleTagManagerEvent(envelope: AnalyticsEnvelope): GoogleEventProjectionResult;
|
|
15
|
+
|
|
16
|
+
declare class GoogleTagManagerProjectionError extends Error {
|
|
17
|
+
readonly issues: readonly _stackonward_analytics_core.AnalyticsValidationIssue[];
|
|
18
|
+
constructor(issues: ReturnType<typeof projectGoogleTagManagerEvent> & {
|
|
19
|
+
success: false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
interface GoogleTagManagerTransportOptions {
|
|
23
|
+
readonly beforeSend?: () => void | Promise<void>;
|
|
24
|
+
readonly push: (event: GoogleTagManagerDataLayerEvent) => void;
|
|
25
|
+
readonly dispose?: () => void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare function createGoogleTagManagerTransport(options: GoogleTagManagerTransportOptions): AnalyticsTransport;
|
|
28
|
+
|
|
29
|
+
export { GoogleTagManagerProjectionError, createGoogleTagManagerTransport, projectGoogleTagManagerEvent };
|
|
30
|
+
export type { GoogleEventProjectionResult, GoogleTagManagerDataLayerEvent, GoogleTagManagerTransportOptions };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as _stackonward_analytics_core from '@stackonward/analytics-core';
|
|
2
|
+
import { AnalyticsPrimitive, AnalyticsEnvelope, AnalyticsValidationIssue, AnalyticsTransport } from '@stackonward/analytics-core';
|
|
3
|
+
|
|
4
|
+
type GoogleTagManagerDataLayerEvent = Readonly<Record<string, AnalyticsPrimitive> & {
|
|
5
|
+
event: string;
|
|
6
|
+
}>;
|
|
7
|
+
type GoogleEventProjectionResult = {
|
|
8
|
+
readonly success: true;
|
|
9
|
+
readonly data: GoogleTagManagerDataLayerEvent;
|
|
10
|
+
} | {
|
|
11
|
+
readonly success: false;
|
|
12
|
+
readonly issues: readonly AnalyticsValidationIssue[];
|
|
13
|
+
};
|
|
14
|
+
declare function projectGoogleTagManagerEvent(envelope: AnalyticsEnvelope): GoogleEventProjectionResult;
|
|
15
|
+
|
|
16
|
+
declare class GoogleTagManagerProjectionError extends Error {
|
|
17
|
+
readonly issues: readonly _stackonward_analytics_core.AnalyticsValidationIssue[];
|
|
18
|
+
constructor(issues: ReturnType<typeof projectGoogleTagManagerEvent> & {
|
|
19
|
+
success: false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
interface GoogleTagManagerTransportOptions {
|
|
23
|
+
readonly beforeSend?: () => void | Promise<void>;
|
|
24
|
+
readonly push: (event: GoogleTagManagerDataLayerEvent) => void;
|
|
25
|
+
readonly dispose?: () => void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
declare function createGoogleTagManagerTransport(options: GoogleTagManagerTransportOptions): AnalyticsTransport;
|
|
28
|
+
|
|
29
|
+
export { GoogleTagManagerProjectionError, createGoogleTagManagerTransport, projectGoogleTagManagerEvent };
|
|
30
|
+
export type { GoogleEventProjectionResult, GoogleTagManagerDataLayerEvent, GoogleTagManagerTransportOptions };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const GOOGLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
|
|
2
|
+
const RESERVED_PREFIXES = ["firebase_", "google_", "ga_"];
|
|
3
|
+
const GOOGLE_PARAMETER_LIMIT = 25;
|
|
4
|
+
function projectGoogleTagManagerEvent(envelope) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
validateGoogleName("name", envelope.name, issues);
|
|
7
|
+
const contextKeys = new Set(Object.keys(envelope.context));
|
|
8
|
+
for (const name of Object.keys(envelope.properties)) {
|
|
9
|
+
if (contextKeys.has(name)) {
|
|
10
|
+
issues.push({
|
|
11
|
+
path: `properties.${name}`,
|
|
12
|
+
code: "context_property_collision",
|
|
13
|
+
message: "Event properties cannot overwrite analytics context"
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const merged = { ...envelope.context, ...envelope.properties, event_source: envelope.source };
|
|
18
|
+
const entries = Object.entries(merged);
|
|
19
|
+
if (entries.length > GOOGLE_PARAMETER_LIMIT) {
|
|
20
|
+
issues.push({
|
|
21
|
+
path: "properties",
|
|
22
|
+
code: "too_many_parameters",
|
|
23
|
+
message: `Google events cannot exceed ${GOOGLE_PARAMETER_LIMIT} parameters`
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
for (const [name, value] of entries) {
|
|
27
|
+
validateGoogleName(`properties.${name}`, name, issues);
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
issues.push({
|
|
30
|
+
path: `properties.${name}`,
|
|
31
|
+
code: "unsupported_array",
|
|
32
|
+
message: "Google Tag Manager event parameters must be primitive values"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (issues.length > 0) {
|
|
37
|
+
return { success: false, issues: Object.freeze(issues) };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
data: Object.freeze({
|
|
42
|
+
event: envelope.name,
|
|
43
|
+
...Object.fromEntries(entries)
|
|
44
|
+
})
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function validateGoogleName(path, value, issues) {
|
|
48
|
+
if (!GOOGLE_NAME_PATTERN.test(value)) {
|
|
49
|
+
issues.push({
|
|
50
|
+
path,
|
|
51
|
+
code: "invalid_google_name",
|
|
52
|
+
message: "Google names must start with a letter and contain at most 40 alphanumeric or underscore characters"
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (RESERVED_PREFIXES.some((prefix) => value.toLowerCase().startsWith(prefix))) {
|
|
56
|
+
issues.push({
|
|
57
|
+
path,
|
|
58
|
+
code: "reserved_google_prefix",
|
|
59
|
+
message: "Google event and parameter names cannot use reserved prefixes"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class GoogleTagManagerProjectionError extends Error {
|
|
65
|
+
issues;
|
|
66
|
+
constructor(issues) {
|
|
67
|
+
super("Analytics event is not compatible with Google Tag Manager");
|
|
68
|
+
this.name = "GoogleTagManagerProjectionError";
|
|
69
|
+
this.issues = issues.issues;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function createGoogleTagManagerTransport(options) {
|
|
73
|
+
let disposed = false;
|
|
74
|
+
return {
|
|
75
|
+
id: "google-tag-manager",
|
|
76
|
+
async send(envelope) {
|
|
77
|
+
if (disposed) {
|
|
78
|
+
throw new Error("Google Tag Manager transport is disposed");
|
|
79
|
+
}
|
|
80
|
+
const projection = projectGoogleTagManagerEvent(envelope);
|
|
81
|
+
if (!projection.success) {
|
|
82
|
+
throw new GoogleTagManagerProjectionError(projection);
|
|
83
|
+
}
|
|
84
|
+
await options.beforeSend?.();
|
|
85
|
+
options.push(projection.data);
|
|
86
|
+
},
|
|
87
|
+
async dispose() {
|
|
88
|
+
if (disposed) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
disposed = true;
|
|
92
|
+
await options.dispose?.();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { GoogleTagManagerProjectionError, createGoogleTagManagerTransport, projectGoogleTagManagerEvent };
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
+
|
|
3
|
+
import type { default as Module } from './module.mjs'
|
|
4
|
+
|
|
5
|
+
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
+
|
|
7
|
+
export { default, type normalizeGoogleTagManagerNuxtOptions } from './module.mjs'
|
|
8
|
+
|
|
9
|
+
export { type GoogleConsentState, type GoogleConsentValue, type GoogleTagManagerNuxtModuleOptions, type GoogleTagManagerNuxtRuntimeConfig, type GoogleTagManagerScriptOptions } from './module.mjs'
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackonward/google-tag-manager-nuxt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Consent-aware Google Tag Manager transport and analytics composition module for Nuxt",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/module.mjs",
|
|
7
|
+
"types": "./dist/module.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/module.d.mts",
|
|
11
|
+
"import": "./dist/module.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./transport": {
|
|
14
|
+
"types": "./dist/transport.d.mts",
|
|
15
|
+
"import": "./dist/transport.mjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/1yhy/stackonward.git",
|
|
31
|
+
"directory": "packages/integrations/google/tag-manager-nuxt"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"analytics",
|
|
35
|
+
"google-tag-manager",
|
|
36
|
+
"gtm",
|
|
37
|
+
"nuxt"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"zod": "^3.24.0",
|
|
42
|
+
"@stackonward/analytics-core": "^0.1.0",
|
|
43
|
+
"@stackonward/analytics-nuxt": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@nuxt/scripts": ">=1.3.0 <2",
|
|
47
|
+
"nuxt": ">=3.15.0 <5",
|
|
48
|
+
"vue": ">=3.5.0 <4"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@nuxt/kit": "^3.15.0",
|
|
52
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
53
|
+
"@nuxt/schema": "^3.15.0",
|
|
54
|
+
"@nuxt/scripts": "^1.3.0",
|
|
55
|
+
"happy-dom": "^17.6.3",
|
|
56
|
+
"nuxt": "^3.20.1",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"unbuild": "^3.5.0",
|
|
59
|
+
"vitest": "^3.2.4",
|
|
60
|
+
"vue": "^3.5.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=20"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "nuxt-module-build build && unbuild",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"test": "vitest run"
|
|
69
|
+
}
|
|
70
|
+
}
|