maxandriani / ngx-google-analytics

An easy way to use and configure Google Analytics on Angular 6+ applications
MIT License
120 stars 49 forks source link

It is working with cookies consent? #40

Closed AlbertMontagutCasero closed 3 years ago

AlbertMontagutCasero commented 3 years ago

Hi in the European Union (EU) we need to notify the users for cookies consent so they have to accept the consent before initializing the cookies for Google analytics. This means that the lib can't be auto initialized.

Is there any way the lib can be initialized by hand?

maxandriani commented 3 years ago

@AlbertMontagutCasero Yes you can!

But first, I recommend you to try the GA's docs approach https://developers.google.com/gtagjs/devguide/consent, you can add thease command on "commands" property of settings, NgxGoogleAnalyticsModule.forRoot('tracking', [['consent'. ...]].

Answering yout question, to lazy initialize this wrapper, you should not import NgxGoogleAnalyticsModule.forRoot() either NgxGoogleAnalyticsRouterModule because NGA Module initializes the script and NGAR has dependency of NGA.

First of all, you should add the follow providers at the highest level module.

@NgModule({
  imports: [NgxGoogleAnalyticsModule], // <<<<<<<< without .forRoot()
  providers: [
    {
       provide: NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN,
       useValue: {
         trackingCode,
         commands,
         uri,
         enableTracing,
         nonce
       } as IGoogleAnalyticsSettings
    }
  ]
})
export class AppModule { ... }

Then, after users concent, you should call the following factories. Make sure to resolve all @injector() dependencies.

GoogleAnalyticsInitializer(
    @import(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN),
    @import(NGX_GTAG_FN),
    @import(DOCUMENT))(); // pay attention on extra parentesis;

GoogleAnalyticsRouterInitializer(
    @import(IGoogleAnalyticsRoutingSettings),
    @import(GoogleAnalyticsService))(ref: ComponentRef<AppComponent>); // ref should be the ComponentRef<> of highest level component (AppComponent).
nyze2oo9 commented 3 years ago

It seems that gtag("consent", "default", ...); needs to be called before gtag("config", "TRACKING-CODE"). In the following code you can see that config will be run before supplied initial commands:

    // these commands should run first!
    const initialCommands: Array<IGoogleAnalyticsCommand> = [
      { command: 'js', values: [ new Date() ] },
      { command: 'config', values: [ settings.trackingCode ] }
    ];

    settings.initCommands = [ ...initialCommands, ...(settings.initCommands || []) ];

    for (const command of settings.initCommands) {
      gtag(command.command, ...command.values);
    }

In my opinion there are two ways to fix this:

  1. split initCommands into beforeInitCommands and afterInitCommands. These will run before or after config.
  2. make consent specific settings.
maxandriani commented 3 years ago

@nyze2oo9 I need to refactor this lib to fully comply consent cookies and privacy laws. But at the moment I don't have much time to work on it. The major features should be Provide a lazy initialization; Privacy issues; and Multiple tracking codes.

In a short time, I can merge initialCommands w/ js and config and it should work. If you provide those commands I'll ignore the merge, if not I'll pop then.

nyze2oo9 commented 3 years ago

@maxandriani I would like to help with the lazy initialization. Have you already thought about how to implement this feature?

In my opinion a Service with inject() and eject() would be needed. Because consent could be revoked by the user at any time.

jadurani commented 2 years ago

To anyone who may be wanting to initialize this google analytics package after loading a config file asynchronously via a ConfigService, the following works too:

@NgModule({
  imports: [NgxGoogleAnalyticsModule], // <<<<<<<< without .forRoot()
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initConfig,
      deps: [ConfigService],
      multi: true,
    },
    {
      provide: NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN,
      useFactory: initGA,
      deps: [ConfigService, NGX_GTAG_FN, DOCUMENT],
      multi: true,
    },
  ]
})

where initGA is:

export async function initGA(
  configService: ConfigService,
  gtag: GtagFn,
  doc: Document
): Promise<void> {
  const gaTag = configService.gaTag;
  if (isNullOrUndefined(gaTag)) {
    console.error('Google analytics tag is not found');
    return;
  }

  return await GoogleAnalyticsInitializer({ trackingCode: gaTag }, gtag, doc)();
}

and initConfig looks like:

export function initConfig(
  configService: ConfigService
): () => Promise<ConfigSettings> {
  return () => configService.init();
}

and configService.init() loads your external config file, maybe from S3 or a local assets file. ✨

red2678 commented 2 years ago

But first, I recommend you to try the GA's docs approach https://developers.google.com/gtagjs/devguide/consent, you can add thease command on "commands" property of settings, NgxGoogleAnalyticsModule.forRoot('tracking', [['consent'. ...]].

@maxandriani are there any instructions that you may know of on how to get this or the lazy method you have shown with Angular 13? I am trying but I just keep getting ts errors. Many thanks for any help!

image

maxandriani commented 2 years ago

@red2678 You can try the import() function instead of @inport() decorator. I'm not aware of ng 13 changes yet.

Gykonik commented 2 years ago

@red2678 I'm also not able to get it running with basically the same errors. I would love to hear, if anyone has a solution for that?

red2678 commented 2 years ago

I ended up not using this solution, but I did get it mostly working, I think. Sorry, I do not have the code anymore, so this will be from memory.

You can get a lot working by using the injector service. constructor(private injector: Injector)

You can then call the .get() method on the injector service.

Example: this.injector.get(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN);

Then use that in the GoogleAnalyticsInitialzer(this.injector.get(NGX_GOOGLE_ANALYTICS_SETTINGS_TOKEN), ...)

I ended up using CookieBot's implementation. I am using Google Tag Manager to implement my scripts. I could not figure out how to load/start the services after a particular event was triggered in GTM. @maxandriani gave an answer on another question that helped me a lot, but I could not get 100% there. I had issues getting the ComponentRef, I think. You need to have the route inject it. I followed other users' responses in this and other threads here to get that going. However, my implementation was a bit flaky and beginning to bloat. I needed to create an intermediary service to manage a lot of this. This was my use case though, and I am sure I was not being efficient in my ways,

I was unhappy with the results, and I needed to move on. It's a bummer. I wanted to use this package as it offers automatic route tracking and has tons of other helper directives. Both of which I had to (and still have to) write myself and are probably not as robust as these. So like I said, I ended up using CookieBot's implementation.

Thank you for the response, @maxandriani. You have done great so far!! A more straightforward implementation with services like CookieBot or an easier way to do what we are doing here would be greatly welcomed. I know you must have a life outside of providing free software to the world, so please, do not take that as a demand but rather a request. Thanks for what you do! Oh, and hi from NC!

I hope this helps you in some way @Gykonik

exitlol commented 2 years ago

@maxandriani I am trying to implement a consent accept service which is provided in the root component. My main issue is how can I get the componentRef of AppComponent inside of the same component without making a new version of said component?

Or how can I init the service without the need of the componentRef. This gives me headaches for days. And I can't seem to find a solution that works.

ApplicationRef returns an empty array, so I cannot use that to get the instance.

dapptain commented 2 years ago

I know you asked the repo owner, but since I was just here, I will post my findings. Webstorm's local changes ftw :)

I created a service to manage this AnalyticsService. Using information from another issue posted here, I got to the below. It will register a factory and use a dependency injection token called APP_BOOTSTRAP_LISTENER (see here) then, in the factory I get the service I made AnalyticsService and the ComponentRef (in this case the AppComponent) and pass them to the service I made to set things up.

app.module.ts

providers: [{
      provide: APP_BOOTSTRAP_LISTENER,
      multi: true,
      useFactory: (analyticsService: AnalyticsService) => (component: ComponentRef<AppComponent>) => {
        // We need to inject the ComponentRef<AppComponent>, created during bootstrapping, into the Analytics service,
        // so we use it to manage the Google Analytics state
        analyticsService.setupGoogleAnalytics(component);
      },
      deps: [AnalyticsService],
    },
  ],

You can then use the ComponentRef in your service.

analytics.service.ts

export class AnalyticsService {
  public setupGoogleAnalytics(bootstrapComponent: ComponentRef<AppComponent>) {
   // do your stuff here
  }
}
maxandriani commented 2 years ago

@exitlol The componentRef of root component it basicaly the component itself. Unfortunately you can not call ComponentRef on DI, but you can try one of two approaches:

a) Fake a componentRef instance by injecting their properties mannually https://angular.io/api/core/ComponentRef. (I'm not a good fan of that one);

b) The only thing I need the componentRef is to have access to injector instance to get Router instance. So you can resolve it by inject Router on RootComponent constructor and copy and paste de content of ga-router-init fn https://github.com/maxandriani/ngx-google-analytics/blob/master/projects/ngx-google-analytics/src/lib/initializers/google-analytics-router.initializer.ts; Just remember to implement ngOnDestroy to clean up subscriptions.

A permanent fix to this issue will be create a orquestrator service to await consent signal to then start GA aumotatically. I'll provide the diagrams soon to achieve this behaviour so anyone can submmit a pr :)

exitlol commented 2 years ago

@maxandriani Thanks a lot for the quick reply. As a usual developer I've managed to fix my issue later. The AppRef returns the correct array in my service sub, so I can pass the correct componentRef to the init methods.

maxandriani commented 2 years ago

Google has develop a beta 'consent' mode, maybe this can fix the major use cases of this thread.

https://github.com/maxandriani/ngx-google-analytics/issues/89

dreaddy commented 9 months ago

Since google pointed me here - another approach: (you need to reload the page after the cookie consent dialog):


// app.module.ts:

// put before @NgModule :

// set somewhere with window.localStorage.setItem("analyticsAllowed", "true");
var allowAnalytics = window.localStorage.getItem('analyticsAllowed') == "true";

let analytics : any[] = [];

if (allowAnalytics) {
  analytics = [
    NgxGoogleAnalyticsRouterModule,
    NgxGoogleAnalyticsModule.forRoot('analytics-id')
  ];
}

// now you can add the array to the imports, that only contains analytics if it is allowed:

   imports: [
        analytics,
        CommonModule,
        [..]