srcdev-nuxt-components 9.4.1 → 9.4.2

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.
@@ -60,9 +60,9 @@ export const useGoogleAnalytics = () => {
60
60
  return;
61
61
  }
62
62
 
63
- const { trigger } = useCookieConsent();
63
+ const { status, trigger } = useCookieConsent();
64
64
 
65
- useScriptGoogleAnalytics({
65
+ const { consent } = useScriptGoogleAnalytics({
66
66
  id,
67
67
  scriptOptions: { trigger },
68
68
  defaultConsent: {
@@ -72,13 +72,31 @@ export const useGoogleAnalytics = () => {
72
72
  analytics_storage: "denied",
73
73
  },
74
74
  });
75
+
76
+ // trigger only gates whether gtag.js *loads* — consent.update() is the separate API that
77
+ // actually reports the visitor's decision back to gtag.js for every hit it sends.
78
+ watch(
79
+ status,
80
+ (value) => {
81
+ if (value === "unset" || !consent) return;
82
+ const granted = value === "granted";
83
+ consent.update({
84
+ ad_storage: granted ? "granted" : "denied",
85
+ ad_user_data: granted ? "granted" : "denied",
86
+ ad_personalization: granted ? "granted" : "denied",
87
+ analytics_storage: granted ? "granted" : "denied",
88
+ });
89
+ },
90
+ { immediate: true }
91
+ );
75
92
  };
76
93
  ```
77
94
 
78
95
  ### Key rules
79
96
 
80
97
  - **`useRuntimeConfig()` inside the function body**, not module scope — same reasoning as `useWhatsApp` (see [[composable-whatsapp]]).
81
- - **`defaultConsent` is all `"denied"`** — this is what makes the script Consent Mode v2 compliant: gtag.js can load (if triggered) but won't set cookies or send identifiable pings until `useCookieConsent().acceptAll()` calls `trigger.accept()`, which flips consent to granted via `@nuxt/scripts`' own consent-update wiring.
98
+ - **`defaultConsent` is all `"denied"`** — sets the initial Consent Mode v2 state before gtag.js has any real signal. `trigger` (passed to `scriptOptions.trigger`) only controls when the script *loads*; it does **not** itself update the Consent Mode signals. Without the `consent.update()` watcher, hits fire successfully once loaded but stay tagged as denied forever, which GA4 excludes from standard reporting — this was a real bug (hits visible in Network, "no data received" in the GA4 dashboard) until the watcher was added.
99
+ - The `watch(status, ..., { immediate: true })` also covers a returning visitor whose `cookie-consent` cookie already says "granted" on this page load, before any click happens.
82
100
  - Does not return anything — it's a side-effecting setup call, not a value composable. Read GA-related state (if ever needed) via `useCookieConsent()` instead.
83
101
 
84
102
  ## Notes
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { nextTick, ref } from "vue";
3
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime";
4
+
5
+ const { useRuntimeConfigMock, updateSpy, useScriptGoogleAnalyticsMock } = vi.hoisted(() => ({
6
+ useRuntimeConfigMock: vi.fn(() => ({ public: { googleAnalytics: { id: "" } }, app: { baseURL: "/" } })),
7
+ updateSpy: vi.fn(),
8
+ useScriptGoogleAnalyticsMock: vi.fn(() => ({ consent: { update: updateSpy, default: vi.fn() } })),
9
+ }));
10
+
11
+ const statusRef = ref<"unset" | "granted" | "denied">("unset");
12
+ const trigger = { consented: ref(false), accept: vi.fn(), revoke: vi.fn() };
13
+
14
+ mockNuxtImport("useRuntimeConfig", () => useRuntimeConfigMock);
15
+ mockNuxtImport("useScriptGoogleAnalytics", () => useScriptGoogleAnalyticsMock);
16
+ mockNuxtImport("useCookieConsent", () => () => ({ status: statusRef, trigger }));
17
+
18
+ const { useGoogleAnalytics } = await import("../useGoogleAnalytics");
19
+
20
+ describe("useGoogleAnalytics", () => {
21
+ beforeEach(() => {
22
+ statusRef.value = "unset";
23
+ updateSpy.mockClear();
24
+ useScriptGoogleAnalyticsMock.mockClear();
25
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "" } }, app: { baseURL: "/" } });
26
+ });
27
+
28
+ it("warns and does nothing when googleAnalytics.id is unconfigured", () => {
29
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
30
+ useGoogleAnalytics();
31
+ expect(warnSpy).toHaveBeenCalledWith("[useGoogleAnalytics] public.googleAnalytics.id is not configured");
32
+ expect(useScriptGoogleAnalyticsMock).not.toHaveBeenCalled();
33
+ warnSpy.mockRestore();
34
+ });
35
+
36
+ it("calls useScriptGoogleAnalytics with the configured id and defaultConsent denied", () => {
37
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "G-TEST123" } }, app: { baseURL: "/" } });
38
+ useGoogleAnalytics();
39
+ expect(useScriptGoogleAnalyticsMock).toHaveBeenCalledWith(
40
+ expect.objectContaining({
41
+ id: "G-TEST123",
42
+ defaultConsent: {
43
+ ad_storage: "denied",
44
+ ad_user_data: "denied",
45
+ ad_personalization: "denied",
46
+ analytics_storage: "denied",
47
+ },
48
+ })
49
+ );
50
+ });
51
+
52
+ it("does not call consent.update() while status is 'unset'", () => {
53
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "G-TEST123" } }, app: { baseURL: "/" } });
54
+ useGoogleAnalytics();
55
+ expect(updateSpy).not.toHaveBeenCalled();
56
+ });
57
+
58
+ it("calls consent.update() with all signals granted once status becomes 'granted'", async () => {
59
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "G-TEST123" } }, app: { baseURL: "/" } });
60
+ useGoogleAnalytics();
61
+ statusRef.value = "granted";
62
+ await nextTick();
63
+ expect(updateSpy).toHaveBeenCalledWith({
64
+ ad_storage: "granted",
65
+ ad_user_data: "granted",
66
+ ad_personalization: "granted",
67
+ analytics_storage: "granted",
68
+ });
69
+ });
70
+
71
+ it("calls consent.update() with all signals denied when status becomes 'denied'", async () => {
72
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "G-TEST123" } }, app: { baseURL: "/" } });
73
+ useGoogleAnalytics();
74
+ statusRef.value = "denied";
75
+ await nextTick();
76
+ expect(updateSpy).toHaveBeenCalledWith({
77
+ ad_storage: "denied",
78
+ ad_user_data: "denied",
79
+ ad_personalization: "denied",
80
+ analytics_storage: "denied",
81
+ });
82
+ });
83
+
84
+ it("immediately reports consent.update() for a returning visitor already granted", () => {
85
+ useRuntimeConfigMock.mockReturnValue({ public: { googleAnalytics: { id: "G-TEST123" } }, app: { baseURL: "/" } });
86
+ statusRef.value = "granted";
87
+ useGoogleAnalytics();
88
+ expect(updateSpy).toHaveBeenCalledWith(expect.objectContaining({ analytics_storage: "granted" }));
89
+ });
90
+ });
@@ -11,9 +11,9 @@ export const useGoogleAnalytics = () => {
11
11
  return;
12
12
  }
13
13
 
14
- const { trigger } = useCookieConsent();
14
+ const { status, trigger } = useCookieConsent();
15
15
 
16
- useScriptGoogleAnalytics({
16
+ const { consent } = useScriptGoogleAnalytics({
17
17
  id,
18
18
  scriptOptions: { trigger },
19
19
  defaultConsent: {
@@ -23,4 +23,26 @@ export const useGoogleAnalytics = () => {
23
23
  analytics_storage: "denied",
24
24
  },
25
25
  });
26
+
27
+ // `trigger` above only gates whether gtag.js *loads* — it does not itself flip the Consent
28
+ // Mode v2 signals gtag.js reports alongside every hit. Without this, hits fire successfully
29
+ // (visible in Network) but stay tagged with defaultConsent's "denied" state forever, which
30
+ // GA4 excludes from standard reporting — confirmed via a real hit's gcs=G100/pscdl=denied
31
+ // query params still showing after accepting, 2026-09-06. `consent.update()` is the separate
32
+ // API that actually reports the visitor's decision back to gtag.js; `immediate: true` also
33
+ // covers a returning visitor whose cookie already says "granted" on this page load.
34
+ watch(
35
+ status,
36
+ (value) => {
37
+ if (value === "unset" || !consent) return;
38
+ const granted = value === "granted";
39
+ consent.update({
40
+ ad_storage: granted ? "granted" : "denied",
41
+ ad_user_data: granted ? "granted" : "denied",
42
+ ad_personalization: granted ? "granted" : "denied",
43
+ analytics_storage: granted ? "granted" : "denied",
44
+ });
45
+ },
46
+ { immediate: true }
47
+ );
26
48
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.4.1",
4
+ "version": "9.4.2",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",