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"`** —
|
|
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
|
};
|