diff --git a/README.md b/README.md index 654c72d..713c1eb 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,25 @@ class CookiesServiceProvider extends ServiceProvider ->session() ->csrf(); - // Register all Analytics cookies at once using one single shorthand method: + // Register all Google Analytics cookies at once using one single shorthand method: + // Don't forget to change the id an anonymizeIp to point to the good location Cookies::analytics() - ->google( - id: config('cookieconsent.google_analytics.id') - anonymizeIp: config('cookieconsent.google_analytics.anonymize_ip') - ); + ->googleAnalytics( + id: config('your_config.google_analytics.id'), + anonymizeIp: config('your_config.google_analytics.anonymize_ip') + ); + + // Using Google Tag Manager? Use this method instead of the Google Analytics one; + Cookies::analytics() + ->googleTagManager( + id: config('your_config.google_tag_manager.id'), + config: [ + 'ad_user_data', + 'ad_personalization', + 'ad_storage', + 'analytics_storage', + ] + ); // Register custom cookies under the pre-existing "optional" category: Cookies::optional() @@ -129,6 +142,8 @@ class CookiesServiceProvider extends ServiceProvider More details on the available [cookie registration](#registering-cookies) methods below. +More details on the [Google Analytics and Google Tag Manager](#analytics-cookies-custom-methods) methods below. + Then, let's add consent scripts and modals to the application's views using the following blade directives: - `@cookieconsentscripts`: used to add the package's default JavaScript and any third-party scripts you need to get the end-user's consent for. @@ -413,6 +428,65 @@ Most of the displayed strings are defined in the `cookieConsent::cookies` transl If not already published, you can edit or fill the translation files using `php artisan vendor:publish --tag=laravel-cookie-consent-lang`, this will copy our translation files to your app's `vendor/cookieConsent` "lang" path. +## Analytics cookies custom methods + +### Google Analytics (GA) + +Register all Google Analytics cookies at once using the shorthand method: + +```php +Cookies::analytics() + ->googleAnalytics( + id: config('services.google_analytics.id'), + anonymizeIp: config('services.google_analytics.anonymize_ip', true) + ); +``` + +### Google Tag Manager (GTM) + +For Google Tag Manager with consent mode v2, use this method instead: + +```php +Cookies::analytics() + ->googleTagManager( + id: id: config('services.google_tag_manager.id'), // Your Google Tag Manager Container ID + config: [ + 'ad_storage', // Enables storage for advertising + 'ad_user_data', // Enables sending user data for advertising + 'ad_personalization', // Enables personalized advertising + 'analytics_storage', // Enables storage for analytics + ] + ); +``` + +You can find all the available consent types on [the Google Tag Manager documentation](https://support.google.com/tagmanager/answer/10718549?hl=en) + +**Config file example (`config/services.php`):** +```php +return [ + 'google_analytics' => [ + 'id' => env('GOOGLE_ANALYTICS_ID'), + 'anonymize_ip' => env('GOOGLE_ANALYTICS_ANONYMIZE_IP', true), + ], + + 'google_tag_manager' => [ + 'id' => env('GOOGLE_TAG_MANAGER_ID'), + ], +]; +``` + +**Environment variables (`.env`):** +```env +GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX +GOOGLE_ANALYTICS_ANONYMIZE_IP=true + +GOOGLE_TAG_MANAGER_ID=GTM-XXXXXXX +``` + +**Note:** When using Google Tag Manager, the consent mode is automatically configured. Users must accept the analytics cookies before GTM scripts are loaded. The default consent state is set to "denied" for all specified consent types until the user gives explicit consent. + +> **Important:** Don't use both `googleAnalytics()` and `googleTagManager()` methods together. Choose one based on your tracking setup. + ## A few useful tips > **Disclaimer**: We are not lawyers. Always check with your legal partners which rules may apply to your project. diff --git a/config/cookieconsent.php b/config/cookieconsent.php index d22ea2a..6e4ace0 100644 --- a/config/cookieconsent.php +++ b/config/cookieconsent.php @@ -72,4 +72,17 @@ 'anonymize_ip' => env('GOOGLE_ANALYTICS_ANONYMIZE_IP', true) ], + /* Google Tag Manager configuration + |-------------------------------------------------------------------------- + | + | If you use Google Tag Manager, you can configure the package to automatically + | load the Google Tag Manager script when the user gives his consent. + | + | The ID parameter is required and represents your Google Tag Manager ID. + | + */ + 'google_tag_manager' => [ + 'id' => env('GOOGLE_TAG_MANAGER_ID'), + ], + ]; diff --git a/src/AnalyticCookiesCategory.php b/src/AnalyticCookiesCategory.php index c81e936..e14a60f 100644 --- a/src/AnalyticCookiesCategory.php +++ b/src/AnalyticCookiesCategory.php @@ -5,33 +5,40 @@ class AnalyticCookiesCategory extends CookiesCategory { const GOOGLE_ANALYTICS = 'ga'; + const GOOGLE_TAG_MANAGER = 'gtm'; + + protected function getGroupCookies($group, $name, $id) + { + $key = str_starts_with($id, 'G-') ? substr($id, 2) : $id; + + return $group->name($name) + ->cookie(fn(Cookie $cookie) => $cookie->name('_ga') + ->duration(2 * 365 * 24 * 60) + ->description(__('cookieConsent::cookies.defaults._ga')) + ) + ->cookie(fn(Cookie $cookie) => $cookie->name('_ga_' . strtoupper($key)) + ->duration(2 * 365 * 24 * 60) + ->description(__('cookieConsent::cookies.defaults._ga_ID')) + ) + ->cookie(fn(Cookie $cookie) => $cookie->name('_gid') + ->duration(24 * 60) + ->description(__('cookieConsent::cookies.defaults._gid')) + ) + ->cookie(fn(Cookie $cookie) => $cookie->name('_gat') + ->duration(1) + ->description(__('cookieConsent::cookies.defaults._gat')) + ); + } /** * Define Google Analytics cookies all at once. */ - public function google(string $id, bool $anonymizeIp = true): static + public function googleAnalytics(string $id, bool $anonymizeIp = true): static { $this->group(function (CookiesGroup $group) use ($anonymizeIp, $id) { - $key = str_starts_with($id, 'G-') ? substr($id, 2) : $id; $anonymizeIp = $anonymizeIp === true ? 'true' : 'false'; - $group->name(static::GOOGLE_ANALYTICS) - ->cookie(fn(Cookie $cookie) => $cookie->name('_ga') - ->duration(2 * 365 * 24 * 60) - ->description(__('cookieConsent::cookies.defaults._ga')) - ) - ->cookie(fn(Cookie $cookie) => $cookie->name('_ga_' . strtoupper($key)) - ->duration(2 * 365 * 24 * 60) - ->description(__('cookieConsent::cookies.defaults._ga_ID')) - ) - ->cookie(fn(Cookie $cookie) => $cookie->name('_gid') - ->duration(24 * 60) - ->description(__('cookieConsent::cookies.defaults._gid')) - ) - ->cookie(fn(Cookie $cookie) => $cookie->name('_gat') - ->duration(1) - ->description(__('cookieConsent::cookies.defaults._gat')) - ) + $this->getGroupCookies(group: $group, name: static::GOOGLE_ANALYTICS, id: $id) ->accepted(fn(Consent $consent) => $consent ->script('') ->script( @@ -40,6 +47,31 @@ public function google(string $id, bool $anonymizeIp = true): static ); }); + return $this; + } + + public function googleTagManager(string $id, $config): static + { + GoogleTagManagerConfig::enable($config); + + $consentSettings = array_fill_keys($config, 'granted'); + + $consentJson = json_encode($consentSettings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $this->group(function (CookiesGroup $group) use ($consentJson, $id) { + $this->getGroupCookies(group: $group, name: static::GOOGLE_TAG_MANAGER, id: $id) + ->accepted(fn(Consent $consent) => $consent + ->script('') + ->script( + '' + ) + ->script( + '' + ) + ); + }); + + return $this; } } diff --git a/src/CookiesManager.php b/src/CookiesManager.php index 58b387c..7f04db1 100644 --- a/src/CookiesManager.php +++ b/src/CookiesManager.php @@ -2,6 +2,7 @@ namespace Whitecube\LaravelCookieConsent; +use Illuminate\Database\Eloquent\Casts\Json; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cookie as CookieFacade; use Symfony\Component\HttpFoundation\Cookie as CookieComponent; @@ -18,6 +19,12 @@ class CookiesManager */ protected ?array $preferences = null; + /** + * Google Tag Manager configuration + */ + protected bool $gtmEnabled = false; + protected array $gtmConfig = []; + /** * Create a new Service Manager instance. */ @@ -182,7 +189,11 @@ public function renderScripts(bool $withDefault = true): string public function getNoticeScripts(bool $withDefault): string { - return $withDefault ? $this->getDefaultScriptTag() : ''; + $output = $withDefault ? $this->getDefaultScriptTag() : ''; + + $output .= $this->getGTMScript(); + + return $output; } protected function getConsentedScripts(bool $withDefault): string @@ -205,6 +216,37 @@ protected function getDefaultScriptTag(): string . '>'; } + /** + * Define default Google Tag Manager script tag; + */ + private function getGTMScript(): string + { + if (GoogleTagManagerConfig::isEnabled()) { + $this->gtmEnabled = true; + $this->gtmConfig = GoogleTagManagerConfig::getConfig(); + } + + if (! $this->gtmEnabled){ + return ''; + } + + $defaultSettings = $this->gtmConfig; + + $defaultSettings = array_fill_keys($defaultSettings, 'denied'); + $defaultSettings['wait_for_update'] = 500; + + $defaultJson = json_encode($defaultSettings, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + return ' '; + } + /** * Output the consent alert/modal for current consent state. */ diff --git a/src/GoogleTagManagerConfig.php b/src/GoogleTagManagerConfig.php new file mode 100644 index 0000000..c269ae8 --- /dev/null +++ b/src/GoogleTagManagerConfig.php @@ -0,0 +1,31 @@ +csrf(); // Register all Analytics cookies at once using one single shorthand method: + // Don't forget to change the config location to match your config file // Cookies::analytics() - // ->google( - // id: config('cookieconsent.google_analytics.id'), - // anonymizeIp: config('cookieconsent.google_analytics.anonymize_ip') + // ->googleAnalytics( + // id: config('your_config.google_analytics.id'), + // anonymizeIp: config('your_config.google_analytics.anonymize_ip') + // ); + + // Cookies::analytics() + // ->googleTagManager( + // id: config('your_config.google_tag_manager.id'), + // config: [ + // 'ad_user_data', + // 'ad_personalization', + // 'ad_storage', + // 'analytics_storage', + // ] // ); // Register custom cookies under the pre-existing "optional" category: @@ -31,4 +43,4 @@ protected function registerCookies(): void // ->duration(120) // ->accepted(fn(Consent $consent, MyDarkmode $darkmode) => $consent->cookie(value: $darkmode->getDefaultValue())); } -} +} \ No newline at end of file diff --git a/tests/Unit/AnalyticCookiesCategoryTest.php b/tests/Unit/AnalyticCookiesCategoryTest.php index 3c26a6c..b844025 100644 --- a/tests/Unit/AnalyticCookiesCategoryTest.php +++ b/tests/Unit/AnalyticCookiesCategoryTest.php @@ -6,7 +6,7 @@ it('can register Google Analytics configuration', function () { $category = new AnalyticCookiesCategory('foo'); - expect($category->google('g-foo'))->toBe($category); + expect($category->googleAnalytics('g-foo'))->toBe($category); expect($group = ($category->getDefined()[0] ?? null))->toBeInstanceOf(CookiesGroup::class); expect($group->hasConsentCallback())->toBeTrue();