@stackonward/cli 1.0.1 → 1.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/dist/index.js CHANGED
@@ -5415,7 +5415,7 @@ async function runApple(mode, options) {
5415
5415
 
5416
5416
  // src/index.ts
5417
5417
  var program = new Command();
5418
- program.name("stackonward").description("Create projects and install reusable components and snippets").version("1.0.1");
5418
+ program.name("stackonward").description("Create projects and install reusable components and snippets").version("1.1.0");
5419
5419
  program.command("init [name]").description("Create a new project from template").option("-t, --template <template>", "Template to use").option("-d, --dir <directory>", "Target directory").option("--skip-install", "Skip dependency installation").option("--skip-git", "Skip git initialization").option("--module <module>", "Go module path (required for go-backend)").option("--author <author>", "Author name").option("-y, --yes", "Use defaults without interactive prompts").action(init);
5420
5420
  program.command("add [items...]").description("Add components or snippets to your project").option("-c, --component", "Add component(s)").option("-s, --snippet", "Add snippet(s)").option("-a, --all", "Add all items in category").option("-y, --yes", "Skip confirmation prompts").option("-o, --overwrite", "Overwrite existing files").option("-p, --path <path>", "Custom install path").action(add);
5421
5421
  program.command("list [type]").description("List available templates, components, or snippets").option("-t, --templates", "List templates").option("-c, --components", "List components").option("-s, --snippets", "List snippets").action(list);
@@ -65,11 +65,14 @@ function openCheckout(): void {
65
65
  customer: props.customer,
66
66
  metadata: props.metadata,
67
67
  });
68
- analytics.track("checkout_started", {
69
- attempt_id: props.attemptId,
70
- currency: currency.value,
71
- value_minor: totalAmountMinor.value,
72
- item_count: props.lineItems.length,
68
+ void analytics.track({
69
+ name: "checkout_started",
70
+ properties: {
71
+ attempt_id: props.attemptId,
72
+ currency: currency.value,
73
+ value_minor: totalAmountMinor.value,
74
+ item_count: props.lineItems.length,
75
+ },
73
76
  });
74
77
  emit("checkout", url);
75
78
  if (import.meta.client) {
@@ -15,7 +15,12 @@ const props = withDefaults(
15
15
  {
16
16
  label: "Continue with Google",
17
17
  disabledLabel: "Google sign-in is not configured",
18
- options: () => ({ theme: "outline", size: "large", text: "continue_with", shape: "rectangular" }),
18
+ options: () => ({
19
+ theme: "outline",
20
+ size: "large",
21
+ text: "continue_with",
22
+ shape: "rectangular",
23
+ }),
19
24
  autoExchange: false,
20
25
  oneTap: false,
21
26
  },
@@ -35,8 +40,9 @@ const loading = ref(false);
35
40
 
36
41
  async function handleCredential(response: GoogleCredentialResponse) {
37
42
  emit("credential", response);
38
- analytics.track("identity_google_credential", {
39
- select_by: response.select_by,
43
+ void analytics.track({
44
+ name: "identity_google_credential",
45
+ ...(response.select_by === undefined ? {} : { properties: { select_by: response.select_by } }),
40
46
  });
41
47
 
42
48
  if (!props.autoExchange || !response.credential) {
@@ -48,7 +54,8 @@ async function handleCredential(response: GoogleCredentialResponse) {
48
54
  const payload = await identity.exchangeCredential(response.credential);
49
55
  emit("authenticated", payload);
50
56
  } catch (error) {
51
- const nextError = error instanceof Error ? error : new Error("Google credential exchange failed");
57
+ const nextError =
58
+ error instanceof Error ? error : new Error("Google credential exchange failed");
52
59
  errorMessage.value = nextError.message;
53
60
  emit("error", nextError);
54
61
  } finally {
@@ -68,7 +75,8 @@ async function renderGoogleButton() {
68
75
  await identity.promptOneTap(handleCredential);
69
76
  }
70
77
  } catch (error) {
71
- const nextError = error instanceof Error ? error : new Error("Google sign-in failed to initialize");
78
+ const nextError =
79
+ error instanceof Error ? error : new Error("Google sign-in failed to initialize");
72
80
  errorMessage.value = nextError.message;
73
81
  emit("error", nextError);
74
82
  }
@@ -2,23 +2,63 @@ export type ConsentValue = "accepted" | "rejected";
2
2
 
3
3
  export function useConsent(storageKey?: string) {
4
4
  const site = useSiteShell();
5
+ const analytics = useAnalytics();
6
+ const analyticsConsent = useAnalyticsConsent();
5
7
  const resolvedStorageKey = computed(() => storageKey ?? site.value.consent.storageKey);
6
8
  const value = ref<ConsentValue | null>(null);
7
9
  const ready = ref(false);
8
10
 
9
- onMounted(() => {
10
- const stored = window.localStorage.getItem(resolvedStorageKey.value);
11
+ function applyStoredValue(stored: string | null): void {
11
12
  value.value = stored === "accepted" || stored === "rejected" ? stored : null;
13
+ if (value.value === "accepted") {
14
+ analyticsConsent.grant();
15
+ } else if (value.value === "rejected") {
16
+ analyticsConsent.deny();
17
+ } else {
18
+ analyticsConsent.reset();
19
+ }
20
+ }
21
+
22
+ function handleStorage(event: StorageEvent): void {
23
+ if (event.key === resolvedStorageKey.value) {
24
+ applyStoredValue(event.newValue);
25
+ }
26
+ }
27
+
28
+ onMounted(() => {
29
+ try {
30
+ applyStoredValue(window.localStorage.getItem(resolvedStorageKey.value));
31
+ } catch {
32
+ value.value = null;
33
+ analyticsConsent.reset();
34
+ }
35
+ window.addEventListener("storage", handleStorage);
12
36
  ready.value = true;
13
37
  });
14
38
 
15
- const visible = computed(() => site.value.consent.enabled && ready.value && value.value === null);
39
+ onBeforeUnmount(() => window.removeEventListener("storage", handleStorage));
40
+
41
+ const visible = computed(
42
+ () =>
43
+ site.value.consent.enabled &&
44
+ analytics.status.value !== "disabled" &&
45
+ ready.value &&
46
+ value.value === null,
47
+ );
16
48
 
17
49
  function setConsent(next: ConsentValue): void {
18
50
  value.value = next;
19
51
  if (import.meta.client) {
20
- window.localStorage.setItem(resolvedStorageKey.value, next);
21
- window.dispatchEvent(new CustomEvent("site:consent", { detail: { value: next } }));
52
+ try {
53
+ window.localStorage.setItem(resolvedStorageKey.value, next);
54
+ } catch {
55
+ // 浏览器拒绝持久化时,本次会话的明确选择仍然生效。
56
+ }
57
+ if (next === "accepted") {
58
+ analyticsConsent.grant();
59
+ } else {
60
+ analyticsConsent.deny();
61
+ }
22
62
  }
23
63
  }
24
64
 
@@ -12,8 +12,11 @@ export default defineNuxtPlugin((nuxtApp) => {
12
12
  identity
13
13
  .promptOneTap((response) => {
14
14
  window.dispatchEvent(new CustomEvent("site:google-credential", { detail: response }));
15
- analytics.track("identity_google_credential", {
16
- select_by: response.select_by,
15
+ void analytics.track({
16
+ name: "identity_google_credential",
17
+ ...(response.select_by === undefined
18
+ ? {}
19
+ : { properties: { select_by: response.select_by } }),
17
20
  });
18
21
  })
19
22
  .catch((error) => {
@@ -241,10 +241,6 @@
241
241
  "path": "site-source/app/components/ui/YoutubeEmbed.vue",
242
242
  "target": "app/components/ui/YoutubeEmbed.vue"
243
243
  },
244
- {
245
- "path": "site-source/app/composables/useAnalytics.ts",
246
- "target": "app/composables/useAnalytics.ts"
247
- },
248
244
  {
249
245
  "path": "site-source/app/composables/useCheckout.ts",
250
246
  "target": "app/composables/useCheckout.ts"
@@ -281,10 +277,6 @@
281
277
  "path": "site-source/app/layouts/default.vue",
282
278
  "target": "app/layouts/default.vue"
283
279
  },
284
- {
285
- "path": "site-source/app/plugins/analytics.client.ts",
286
- "target": "app/plugins/analytics.client.ts"
287
- },
288
280
  {
289
281
  "path": "site-source/app/plugins/google-identity.client.ts",
290
282
  "target": "app/plugins/google-identity.client.ts"
@@ -5,11 +5,23 @@
5
5
  "type": "npm",
6
6
  "version": "^0.0.2"
7
7
  },
8
+ "@stackonward/analytics-core": {
9
+ "name": "@stackonward/analytics-core",
10
+ "description": "Provider-neutral analytics pipeline and browser autocapture",
11
+ "type": "npm",
12
+ "version": "^0.1.0"
13
+ },
14
+ "@stackonward/analytics-nuxt": {
15
+ "name": "@stackonward/analytics-nuxt",
16
+ "description": "Provider-neutral Nuxt analytics lifecycle",
17
+ "type": "npm",
18
+ "version": "^0.1.0"
19
+ },
8
20
  "@stackonward/cli": {
9
21
  "name": "@stackonward/cli",
10
22
  "description": "StackOnward project and resource CLI",
11
23
  "type": "npm",
12
- "version": "^1.0.1"
24
+ "version": "^1.1.0"
13
25
  },
14
26
  "@stackonward/cms-client": {
15
27
  "name": "@stackonward/cms-client",
@@ -53,6 +65,12 @@
53
65
  "type": "npm",
54
66
  "version": "^0.0.1"
55
67
  },
68
+ "@stackonward/google-tag-manager-nuxt": {
69
+ "name": "@stackonward/google-tag-manager-nuxt",
70
+ "description": "Consent-aware Google Tag Manager analytics integration for Nuxt",
71
+ "type": "npm",
72
+ "version": "^0.1.0"
73
+ },
56
74
  "@stackonward/identity-browser": {
57
75
  "name": "@stackonward/identity-browser",
58
76
  "description": "Browser client for opaque identity sessions",
@@ -87,7 +105,7 @@
87
105
  "name": "@stackonward/nuxt-site-shell",
88
106
  "description": "Brand-free Nuxt product-site shell",
89
107
  "type": "layer",
90
- "version": "^0.0.3"
108
+ "version": "^0.1.0"
91
109
  },
92
110
  "@stackonward/onex-cms-client": {
93
111
  "name": "@stackonward/onex-cms-client",
@@ -30,18 +30,18 @@ ESLint composes the Nuxt-generated flat configuration with `@stackonward/eslint-
30
30
 
31
31
  `nuxt.config.ts` owns public runtime mapping; `.env.example` documents available values. Public means readable by browser users.
32
32
 
33
- | Variable family | Consumer and meaning |
34
- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
35
- | `NUXT_PUBLIC_API_BASE_URL`, `NUXT_PUBLIC_API_TIMEOUT` | Public API URL and timeout configuration |
36
- | `NUXT_PUBLIC_SITE_URL`, `SITE_ENV`, `SITE_NAME`, `SITE_DESCRIPTION` | Canonical site identity and environment under the same prefix |
37
- | `NUXT_PUBLIC_ANALYTICS_ENABLED`, `ANALYTICS_DEBUG` | Analytics enable/debug configuration |
38
- | `NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT`, `ANALYTICS_PAGE_TRACKING` | Consent requirement and route tracking |
39
- | `NUXT_PUBLIC_ANALYTICS_DATA_LAYER`, `GA_ID`, `GTM_ID` | Public analytics integration IDs under the same prefix |
40
- | `NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED`, `GOOGLE_CLIENT_ID` | Google client-side identity configuration |
41
- | `NUXT_PUBLIC_GOOGLE_CREDENTIAL_ENDPOINT`, `GOOGLE_ONE_TAP`, `GOOGLE_AUTO_PROMPT` | Configured credential exchange endpoint and prompting options |
42
- | `NUXT_PUBLIC_CHECKOUT_URL`, `CHECKOUT_RESULT_PARAM` | Checkout destination and return parameter |
43
- | `NUXT_PUBLIC_YOUTUBE_PRIVACY_ENHANCED` | Video privacy configuration |
44
- | `NUXT_PUBLIC_OG_IMAGE_ENABLED` | SEO layer option; verify the installed layer consumer before changing behavior |
33
+ | Variable family | Consumer and meaning |
34
+ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
35
+ | `NUXT_PUBLIC_API_BASE_URL`, `NUXT_PUBLIC_API_TIMEOUT` | Public API URL and timeout configuration |
36
+ | `NUXT_PUBLIC_SITE_URL`, `SITE_ENV`, `SITE_NAME`, `SITE_DESCRIPTION` | Canonical site identity and environment under the same prefix |
37
+ | `NUXT_PUBLIC_ANALYTICS_DEBUG`, `NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT` | Analytics diagnostics and consent policy |
38
+ | `NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING`, `NUXT_PUBLIC_ANALYTICS_DATA_LAYER` | SPA page capture and GTM data-layer configuration |
39
+ | `NUXT_PUBLIC_GTM_ID` | Public Google Tag Manager container ID; an empty value disables the integration |
40
+ | `NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED`, `GOOGLE_CLIENT_ID` | Google client-side identity configuration |
41
+ | `NUXT_PUBLIC_GOOGLE_CREDENTIAL_ENDPOINT`, `GOOGLE_ONE_TAP`, `GOOGLE_AUTO_PROMPT` | Configured credential exchange endpoint and prompting options |
42
+ | `NUXT_PUBLIC_CHECKOUT_URL`, `CHECKOUT_RESULT_PARAM` | Checkout destination and return parameter |
43
+ | `NUXT_PUBLIC_YOUTUBE_PRIVACY_ENHANCED` | Video privacy configuration |
44
+ | `NUXT_PUBLIC_OG_IMAGE_ENABLED` | SEO layer option; verify the installed layer consumer before changing behavior |
45
45
 
46
46
  Never place OAuth secrets, service credentials or provider API keys here. An enabled client feature does not prove backend credentials or permission are configured. Validate numeric input at its actual consumer rather than relying on `Number.parseInt` alone.
47
47
 
@@ -4,12 +4,10 @@ NUXT_PUBLIC_SITE_URL=
4
4
  NUXT_PUBLIC_SITE_ENV=development
5
5
  NUXT_PUBLIC_SITE_NAME="{{projectName}}"
6
6
  NUXT_PUBLIC_SITE_DESCRIPTION="Reusable Nuxt product-site foundation."
7
- NUXT_PUBLIC_ANALYTICS_ENABLED=false
8
7
  NUXT_PUBLIC_ANALYTICS_DEBUG=false
9
8
  NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT=true
10
9
  NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING=true
11
10
  NUXT_PUBLIC_ANALYTICS_DATA_LAYER=dataLayer
12
- NUXT_PUBLIC_GA_ID=
13
11
  NUXT_PUBLIC_GTM_ID=
14
12
  NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED=false
15
13
  NUXT_PUBLIC_GOOGLE_CLIENT_ID=
@@ -5,12 +5,19 @@ This template extends `@stackonward/nuxt-site-shell` for Nuxt module wiring and
5
5
  - Nuxt UI and Tailwind theme wiring.
6
6
  - Local source files for marketing, app, content and static page shells under `app/layouts` and `app/components`.
7
7
  - Editable base components such as `BaseButton`, `BaseModal`, `BaseText`, `BasePopover`, `GoogleSignInButton`, `YoutubeEmbed`, `InfiniteScroll`, `CheckoutSummary` and `CheckoutStatus`.
8
- - Editable composables and plugins for Google Identity, GA/GTM analytics, consent-gated tracking, privacy-enhanced YouTube embeds and provider-free checkout redirects.
8
+ - Editable composables and plugins for Google Identity, consent presentation, privacy-enhanced YouTube embeds and provider-free checkout redirects.
9
+ - Versioned global analytics through `@stackonward/google-tag-manager-nuxt`, including consent gating, SPA page views, element/link autocapture, manual events and product-configurable capture rules.
9
10
  - Local Nuxt UI theme overrides under `app/theme` and CSS tokens under `app/assets/css`.
10
11
  - A generated `/ui` page showing how to use the included theme and components.
11
12
 
12
13
  Product-specific copy, assets, auth, analytics, CMS providers and business workflows belong in the generated application. Source is copied into the project so product teams can edit it directly.
13
14
 
15
+ ## Analytics
16
+
17
+ Set `NUXT_PUBLIC_GTM_ID` to a published GTM container such as `GTM-ABC123`. The integration remains disabled when the ID is empty. The included consent UI restores the product-owned local decision and starts analytics only after acceptance.
18
+
19
+ Global capture defaults and product overrides live under `googleTagManager.analytics` in `nuxt.config.ts`. Products can configure selectors, internal domains, tracked link types, module naming/rules, event names, device breakpoints and privacy-sensitive field capture. Business event names remain typed in product source. The remote GTM container owns GA4 tags, triggers and consent checks.
20
+
14
21
  ## Engineering workflow
15
22
 
16
23
  ```bash
@@ -42,8 +42,9 @@ const visibleInfiniteItems = computed(() => infiniteItems.slice(0, visibleItems.
42
42
  const hasMoreInfiniteItems = computed(() => visibleItems.value < infiniteItems.length);
43
43
 
44
44
  function trackPreviewEvent() {
45
- analytics.track("creative_system_action", {
46
- surface: "design_system",
45
+ void analytics.track({
46
+ name: "creative_system_action",
47
+ properties: { surface: "design_system" },
47
48
  });
48
49
  }
49
50
 
@@ -1,8 +1,11 @@
1
1
  import { defineNuxtConfig } from "nuxt/config";
2
2
 
3
+ const siteEnv = process.env.NUXT_PUBLIC_SITE_ENV || "development";
4
+ const gtmId = process.env.NUXT_PUBLIC_GTM_ID || "";
5
+
3
6
  export default defineNuxtConfig({
4
7
  extends: ["@stackonward/nuxt-site-shell"],
5
- modules: ["@nuxt/eslint"],
8
+ modules: ["@nuxt/eslint", "@stackonward/google-tag-manager-nuxt"],
6
9
  compatibilityDate: "2025-07-15",
7
10
  css: ["~/assets/css/site-shell.css"],
8
11
  components: [
@@ -14,6 +17,23 @@ export default defineNuxtConfig({
14
17
  imports: {
15
18
  dirs: ["~/composables"],
16
19
  },
20
+ googleTagManager: {
21
+ id: gtmId,
22
+ dataLayerName: process.env.NUXT_PUBLIC_ANALYTICS_DATA_LAYER || "dataLayer",
23
+ analytics: {
24
+ applicationId: "{{projectName}}",
25
+ environment: siteEnv,
26
+ debug: process.env.NUXT_PUBLIC_ANALYTICS_DEBUG === "true",
27
+ consent: {
28
+ required: process.env.NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT !== "false",
29
+ },
30
+ autoCapture: {
31
+ pageViews: process.env.NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING !== "false",
32
+ modulePrefix: "",
33
+ moduleRules: [],
34
+ },
35
+ },
36
+ },
17
37
  icon: {
18
38
  customCollections: [
19
39
  {
@@ -34,20 +54,11 @@ export default defineNuxtConfig({
34
54
  },
35
55
  site: {
36
56
  url: process.env.NUXT_PUBLIC_SITE_URL || "",
37
- env: process.env.NUXT_PUBLIC_SITE_ENV || "development",
57
+ env: siteEnv,
38
58
  name: process.env.NUXT_PUBLIC_SITE_NAME || "{{projectName}}",
39
59
  description:
40
60
  process.env.NUXT_PUBLIC_SITE_DESCRIPTION || "Premium product media production workspace.",
41
61
  },
42
- analytics: {
43
- enabled: process.env.NUXT_PUBLIC_ANALYTICS_ENABLED === "true",
44
- debug: process.env.NUXT_PUBLIC_ANALYTICS_DEBUG === "true",
45
- requireConsent: process.env.NUXT_PUBLIC_ANALYTICS_REQUIRE_CONSENT !== "false",
46
- pageTracking: process.env.NUXT_PUBLIC_ANALYTICS_PAGE_TRACKING !== "false",
47
- dataLayerName: process.env.NUXT_PUBLIC_ANALYTICS_DATA_LAYER || "dataLayer",
48
- gaId: process.env.NUXT_PUBLIC_GA_ID || "",
49
- gtmId: process.env.NUXT_PUBLIC_GTM_ID || "",
50
- },
51
62
  identity: {
52
63
  google: {
53
64
  enabled: process.env.NUXT_PUBLIC_GOOGLE_IDENTITY_ENABLED === "true",
@@ -29,8 +29,9 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@stackonward/api-client": "^0.0.2",
32
+ "@stackonward/google-tag-manager-nuxt": "^0.1.0",
32
33
  "@stackonward/nuxt-layer-base": "^0.0.3",
33
- "@stackonward/nuxt-site-shell": "^0.0.3"
34
+ "@stackonward/nuxt-site-shell": "^0.1.0"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@commitlint/cli": "^21.2.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackonward/cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "StackOnward CLI for creating projects and installing reusable components and snippets",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,24 +0,0 @@
1
- export interface AnalyticsEvent {
2
- name: string;
3
- properties?: Record<string, unknown>;
4
- }
5
-
6
- export function useAnalytics() {
7
- function track(event: AnalyticsEvent | string, properties?: Record<string, unknown>): void {
8
- if (!import.meta.client) return;
9
- const detail = typeof event === "string" ? { name: event, properties } : event;
10
- window.dispatchEvent(new CustomEvent("site:analytics", { detail }));
11
- }
12
-
13
- function pageView(path: string, title?: string): void {
14
- track({
15
- name: "page_view",
16
- properties: {
17
- page_path: path,
18
- page_title: title,
19
- },
20
- });
21
- }
22
-
23
- return { track, pageView };
24
- }
@@ -1,121 +0,0 @@
1
- type DataLayerEvent = Record<string, unknown> & {
2
- event?: string;
3
- };
4
-
5
- declare global {
6
- interface Window {
7
- dataLayer?: DataLayerEvent[];
8
- gtag?: (...args: unknown[]) => void;
9
- __siteAnalyticsInstalled?: boolean;
10
- }
11
- }
12
-
13
- function appendScript(id: string, src: string): void {
14
- if (document.getElementById(id)) {
15
- return;
16
- }
17
-
18
- const script = document.createElement("script");
19
- script.id = id;
20
- script.async = true;
21
- script.src = src;
22
- document.head.appendChild(script);
23
- }
24
-
25
- export default defineNuxtPlugin((nuxtApp) => {
26
- const config = useRuntimeConfig();
27
- const site = useSiteShell();
28
- const analytics = config.public.analytics;
29
-
30
- if (!analytics?.enabled || window.__siteAnalyticsInstalled) {
31
- return;
32
- }
33
-
34
- const dataLayerName = analytics.dataLayerName || "dataLayer";
35
- const win = window as unknown as Window & Record<string, unknown>;
36
-
37
- function dataLayer(): DataLayerEvent[] {
38
- const existing = win[dataLayerName];
39
- if (Array.isArray(existing)) {
40
- return existing as DataLayerEvent[];
41
- }
42
-
43
- const next: DataLayerEvent[] = [];
44
- win[dataLayerName] = next;
45
- if (dataLayerName === "dataLayer") {
46
- window.dataLayer = next;
47
- }
48
- return next;
49
- }
50
-
51
- function push(event: DataLayerEvent): void {
52
- dataLayer().push(event);
53
- if (analytics.debug) {
54
- window.dispatchEvent(new CustomEvent("site:analytics-debug", { detail: event }));
55
- }
56
- }
57
-
58
- function hasConsent(): boolean {
59
- if (!analytics.requireConsent) {
60
- return true;
61
- }
62
- return window.localStorage.getItem(site.value.consent.storageKey) === "accepted";
63
- }
64
-
65
- function install(): void {
66
- if (window.__siteAnalyticsInstalled || !hasConsent()) {
67
- return;
68
- }
69
-
70
- window.__siteAnalyticsInstalled = true;
71
-
72
- if (analytics.gtmId) {
73
- push({ "gtm.start": Date.now(), event: "gtm.js" });
74
- appendScript(
75
- "site-gtm",
76
- `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(analytics.gtmId)}`,
77
- );
78
- }
79
-
80
- if (analytics.gaId) {
81
- window.gtag = (...args: unknown[]) => {
82
- dataLayer().push(args as unknown as DataLayerEvent);
83
- };
84
- appendScript(
85
- "site-gtag",
86
- `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(analytics.gaId)}`,
87
- );
88
- window.gtag("js", new Date());
89
- window.gtag("config", analytics.gaId, { send_page_view: false });
90
- }
91
- }
92
-
93
- window.addEventListener("site:consent", (event) => {
94
- const detail = (event as CustomEvent<{ value?: string }>).detail;
95
- if (detail?.value === "accepted") {
96
- install();
97
- }
98
- });
99
-
100
- window.addEventListener("site:analytics", (event) => {
101
- const detail = (event as CustomEvent<{ name?: string; properties?: Record<string, unknown> }>)
102
- .detail;
103
- if (!detail?.name) {
104
- return;
105
- }
106
- push({ event: detail.name, ...(detail.properties || {}) });
107
- });
108
-
109
- if (analytics.pageTracking) {
110
- const router = useRouter();
111
- router.afterEach((to) => {
112
- push({
113
- event: "page_view",
114
- page_path: to.fullPath,
115
- page_title: document.title,
116
- });
117
- });
118
- }
119
-
120
- nuxtApp.hook("app:mounted", install);
121
- });