Closed AlbertMontagutCasero closed 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).
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:
@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.
@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.
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. ✨
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!
@red2678 You can try the import()
function instead of @inport()
decorator. I'm not aware of ng 13 changes yet.
@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?
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
@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.
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
}
}
@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 :)
@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.
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
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,
[..]
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?