orestbida / cookieconsent

:cookie: Simple cross-browser cookie-consent plugin written in vanilla js
https://playground.cookieconsent.orestbida.com/
MIT License
3.68k stars 387 forks source link

[Bug]: Uncaught (in promise) TypeError: Cannot read properties of null (reading 'appendChild') #676

Open mrleblanc101 opened 2 months ago

mrleblanc101 commented 2 months ago

Expected Behavior

I'm trying to implement CookieConsent with GTM Consent Mode. Here is my config:

<script>
    window.dataLayer = window.dataLayer || [];
    function gtag() { window.dataLayer.push(arguments); }

    gtag('consent', 'default', {
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
        analytics_storage: 'denied',
        functionality_storage: 'denied',
        personalization_storage: 'denied',
        security_storage: 'denied',
    });

    gtag('consent', 'update', {
        functionality_storage: CookieConsent.acceptedCategory('functionality') ? 'granted' : 'denied',
        ad_storage: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
        ad_user_data: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
        ad_personalization: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
        analytics_storage: CookieConsent.acceptedCategory('analytics') ? 'granted' : 'denied',
        personalization_storage: CookieConsent.acceptedCategory('personalization') ? 'granted' : 'denied',
        security_storage: CookieConsent.acceptedCategory('security') ? 'granted' : 'denied',
    });
    CookieConsent.run({
        guiOptions: {
            consentModal: {
                layout: 'box wide',
                position: 'bottom right',
                equalWeightButtons: true,
                flipButtons: true,
            },
            preferencesModal: {
                layout: 'box',
                position: 'right',
                equalWeightButtons: true,
                flipButtons: false,
            },
        },
        categories: {
            necessary: {
                readOnly: true,
            },
            marketing: {},
            analytics: {},
            functionality: {},
            personalization: {},
            security: {},
        },
        language: { ... }
        onConsent: (...args) => {
            manageConsent(args);
            window.dataLayer.push({ event: 'consent_ready' });
        },
        onChange: (...args) => {
            manageConsent(args);
            window.dataLayer.push({ event: 'consent_update' });
        },
    });

    function manageConsent() {
        gtag('consent', 'update', {
            functionality_storage: CookieConsent.acceptedCategory('functionality') ? 'granted' : 'denied',
            ad_storage: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            ad_user_data: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            ad_personalization: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            analytics_storage: CookieConsent.acceptedCategory('analytics') ? 'granted' : 'denied',
            personalization_storage: CookieConsent.acceptedCategory('personalization') ? 'granted' : 'denied',
            security_storage: CookieConsent.acceptedCategory('security') ? 'granted' : 'denied',
        });
    }
</script>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-XXXXXXXX');</script>

Current Behavior

I understand that the errors is because the plugin try to insert the banner into the DOM, while the page has not finished loading (DOMContentLoaded).

My issue is that if I put CookieConsent.run(...) inside a window.addEventListener('load', () => { ... }), the Container Loaded event will be fired before GTM receive the consent value from gtag() inside onConsent, which mean Facebook Pixel is blocked even if the user clicked Accept All.

Screenshot 2024-04-23 at 9 37 48 AM

Screenshot 2024-04-23 at 9 37 57 AM

Steps to reproduce

.

Proposed fix or additional info.

Would it be possible to execute the "cc.run()" command as soon as possible, and only delay the insertion of the banner into the DOM once the page is loaded ? Currently, I need to wrap the whole cc.run() inside the window.addEventListener('load', () => { ... }) which delay the plugin initialization which does not require access to DOM and could be executed earlier.

Version

3.0.1

On which browser do you see the issue?

Chrome

mrleblanc101 commented 2 months ago

Also, if I run CookieConsent.acceptedCategory('functionality') ? 'granted' : 'denied' before CookieConsent.run(...), it will always return denied even if the cookie is set !

mrleblanc101 commented 2 months ago

Should I move all my tags from All pages trigger to DOM Ready or Window Loaded instead ? This seems odd as All pages is the default for GA4 when using to Google Template.

orestbida commented 2 months ago

Would it be possible to execute the "cc.run()" command as soon as possible

By default the plugin is loaded asynchronously, but you can change this behaviour via the lazyHtmlGeneration option.

Also, if I run CookieConsent.acceptedCategory('functionality') ? 'granted' : 'denied' before CookieConsent.run(...), it will always return denied even if the cookie is set !

Yes, that's the expected behaviour; the only method that can be run before the plugin's own initialization is getCookie. You can access the acceptedCategories before the plugin's initialization like this:

const acceptedCategories = CookieConsent.getCookie('categories', 'cc_cookie') || [];
// acceptedCategories.includes('analytics')

CookieConsent.run({...});
mrleblanc101 commented 2 months ago

@orestbida

By default the plugin is loaded asynchronously, but you can change this behaviour via the lazyHtmlGeneration option.

Maybe I'm not understanding correctly, lazyHtmlGeneration seems to default to true in the documentation, yet it I still have the error. I also tried explicitely:

CookieConsent.run({
    autoShow: false,
    lazyHtmlGeneration: true,
    ...
});

Yes, that's the expected behaviour; the only method that can be run before the plugin's own initialization is getCookie. You can access the acceptedCategories before the plugin's initialization like this:

Thanks, I guess that will do, but I think it would be nice to decouple initialization and injection into the DOM. I thought about reading the cookie manually using a cookie library like js-cookie, but I thought it was overkill. Nice to know I can use CookieConsent.getCookie before init.

mrleblanc101 commented 2 months ago

Let me know If you want me to close this or you'd like to keep it open for doc/exemple update. Here is my final results if it can help someone else:

<script src="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.umd.js"></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag() { window.dataLayer.push(arguments); }

    gtag('consent', 'default', {
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
        analytics_storage: 'denied',
        functionality_storage: 'denied',
        personalization_storage: 'denied',
        security_storage: 'denied',
    });

    if(CookieConsent.getCookie('categories', 'cc_cookie')) {
        const acceptedCategories = CookieConsent.getCookie('categories', 'cc_cookie') || [];
        gtag('consent', 'update', {
            functionality_storage: acceptedCategories.includes('functionality') ? 'granted' : 'denied',
            ad_storage: acceptedCategories.includes('marketing') ? 'granted' : 'denied',
            ad_user_data: acceptedCategories.includes('marketing') ? 'granted' : 'denied',
            ad_personalization: acceptedCategories.includes('marketing') ? 'granted' : 'denied',
            analytics_storage: acceptedCategories.includes('analytics') ? 'granted' : 'denied',
            personalization_storage: acceptedCategories.includes('personalization') ? 'granted' : 'denied',
            security_storage: acceptedCategories.includes('security') ? 'granted' : 'denied',
        });
    }

    window.addEventListener('load', function () {
        CookieConsent.run({
            guiOptions: {
                consentModal: {
                    layout: 'box wide',
                    position: 'bottom right',
                    equalWeightButtons: true,
                    flipButtons: true,
                },
                preferencesModal: {
                    layout: 'box',
                    position: 'right',
                    equalWeightButtons: true,
                    flipButtons: false,
                },
            },
            categories: {
                necessary: {
                    readOnly: true,
                },
                marketing: {},
                analytics: {},
                functionality: {},
                personalization: {},
                security: {},
            },
            language: { ... },
            onFirstConsent: (...args) => {
                manageConsent(args);
                window.dataLayer.push({ event: 'consent_ready' });
            },
            onChange: (...args) => {
                manageConsent(args);
                window.dataLayer.push({ event: 'consent_update' });
            },
        });
    });

    function manageConsent() {
        gtag('consent', 'update', {
            functionality_storage: CookieConsent.acceptedCategory('functionality') ? 'granted' : 'denied',
            ad_storage: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            ad_user_data: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            ad_personalization: CookieConsent.acceptedCategory('marketing') ? 'granted' : 'denied',
            analytics_storage: CookieConsent.acceptedCategory('analytics') ? 'granted' : 'denied',
            personalization_storage: CookieConsent.acceptedCategory('personalization') ? 'granted' : 'denied',
            security_storage: CookieConsent.acceptedCategory('security') ? 'granted' : 'denied',
        });
    }
</script>
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-XXXXXXXX');</script>
orestbida commented 2 months ago

Perhaps I wasn't clear, you should set lazyHtmlGeneration to false to run the plugin synchronously.

We can leave this open, until there is a docs. section on how to set up GTM.

mrleblanc101 commented 2 months ago

@orestbida I tried false too and i had the exact same issue, but anyway I posted my solution above.

Thanks again for your help