diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 468ae88e0c6..0dc0b8ad435 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,11 +1,12 @@ import { DOCUMENT } from "@angular/common"; -import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { Component, inject, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { AnalyticsService } from "@bitwarden/angular/analytics/analytics.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -60,6 +61,8 @@ export class AppComponent implements OnDestroy, OnInit { private isIdle = false; private destroy$ = new Subject(); + private analyticsService = inject(AnalyticsService); + loading = false; constructor( @@ -267,6 +270,8 @@ export class AppComponent implements OnDestroy, OnInit { new DisableSendPolicy(), new SendOptionsPolicy(), ]); + + void this.analyticsService.init(); } ngOnDestroy() { diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index df325015aad..f6c7153e3f6 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -299,6 +299,8 @@ const devServer = https://api.fastmail.com https://api.forwardemail.net http://localhost:5000 + https://plausible.io/api/event + ;object-src 'self' blob: diff --git a/libs/angular/src/analytics/analytics.service.ts b/libs/angular/src/analytics/analytics.service.ts new file mode 100644 index 00000000000..284e5f92a47 --- /dev/null +++ b/libs/angular/src/analytics/analytics.service.ts @@ -0,0 +1,48 @@ +import { inject, Injectable } from "@angular/core"; +import Plausible from "plausible-tracker"; +// import { firstValueFrom } from "rxjs"; + +import { ANALYTICS, GlobalStateProvider, KeyDefinition } from "@bitwarden/common/platform/state"; + +const ANALYTICS_ENABLED_KEY_DEF = new KeyDefinition(ANALYTICS, "analytics_enabled", { + deserializer: (s) => s, +}); + +@Injectable({ providedIn: "root" }) +export class AnalyticsService { + private plausible: ReturnType; + private plausibleCleanup?: () => any; + + private readonly _enabledState = inject(GlobalStateProvider).get(ANALYTICS_ENABLED_KEY_DEF); + readonly enabled$ = this._enabledState.state$; + + async init() { + this.plausible = Plausible({ + domain: "bitwarden", + trackLocalhost: true, + hashMode: true, + }); + + // TODO: uncomment when a toggle is added to settings page + // const enabled = await firstValueFrom(this._enabledState.state$); + // if (!enabled) { + // return; + // } + + await this.enable(); + } + + async enable() { + await this._enabledState.update((_prevState) => true); + this.plausibleCleanup = this.plausible.enableAutoPageviews(); + } + + async disable() { + await this._enabledState.update((_prevState) => false); + this?.plausibleCleanup(); + } + + trackEvent(eventName: string) { + this.plausible.trackEvent(eventName); + } +} diff --git a/libs/angular/src/analytics/track-event.directive.ts b/libs/angular/src/analytics/track-event.directive.ts new file mode 100644 index 00000000000..6bb6d599681 --- /dev/null +++ b/libs/angular/src/analytics/track-event.directive.ts @@ -0,0 +1,22 @@ +import { Directive, HostListener, inject, Input } from "@angular/core"; + +import { AnalyticsService } from "./analytics.service"; + +@Directive({ + selector: "[track-event]", + standalone: true, +}) +export class TrackEventDirective { + private analyticsService = inject(AnalyticsService); + + @Input({ + alias: "track-event", + required: true, + }) + eventName!: string; + + @HostListener("click") + handleClick() { + this.analyticsService.trackEvent(this.eventName); + } +} diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index bebac42fd83..3bca619140f 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -27,6 +27,7 @@ import { TypographyModule, } from "@bitwarden/components"; +import { TrackEventDirective } from "./analytics/track-event.directive"; import { TwoFactorIconComponent } from "./auth/components/two-factor-icon.component"; import { DeprecatedCalloutComponent } from "./components/callout.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; @@ -81,6 +82,7 @@ import { IconComponent } from "./vault/components/icon.component"; IconModule, LinkModule, IconModule, + TrackEventDirective, ], declarations: [ A11yInvalidDirective, diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index a600901df4f..8972b8a61d4 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -124,6 +124,7 @@ export const TASK_SCHEDULER_DISK = new StateDefinition("taskScheduler", "disk"); // Design System export const POPUP_STYLE_DISK = new StateDefinition("popupStyle", "disk"); +export const ANALYTICS = new StateDefinition("analytics", "disk"); // Secrets Manager diff --git a/package-lock.json b/package-lock.json index 36b494c00c0..9f850dcea7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "open": "8.4.2", "papaparse": "5.4.1", "patch-package": "8.0.0", + "plausible-tracker": "^0.3.9", "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3", @@ -27174,6 +27175,14 @@ "node": ">=4" } }, + "node_modules/plausible-tracker": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.9.tgz", + "integrity": "sha512-hMhneYm3GCPyQon88SZrVJx+LlqhM1kZFQbuAgXPoh/Az2YvO1B6bitT9qlhpiTdJlsT5lsr3gPmzoVjb5CDXA==", + "engines": { + "node": ">=10" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 7d418b27d0c..06971626554 100644 --- a/package.json +++ b/package.json @@ -191,6 +191,7 @@ "open": "8.4.2", "papaparse": "5.4.1", "patch-package": "8.0.0", + "plausible-tracker": "^0.3.9", "popper.js": "1.16.1", "proper-lockfile": "4.1.2", "qrcode-parser": "2.1.3",