mirror of
https://github.com/bitgapp/eqMac.git
synced 2024-11-22 13:07:26 +03:00
added analytics/telemetry consent and setting
This commit is contained in:
parent
dc8612c0a3
commit
51ee72a70b
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "1.3.4",
|
||||
"version": "1.4.1",
|
||||
"scripts": {
|
||||
"lint": "npx eslint .",
|
||||
"start": "ng serve --port 8080 --host 0.0.0.0 --disable-host-check",
|
||||
|
@ -5,13 +5,14 @@ import {
|
||||
AfterContentInit
|
||||
} from '@angular/core'
|
||||
import { UtilitiesService } from './services/utilities.service'
|
||||
import { UIService, UIDimensions } from './services/ui.service'
|
||||
import { UIService, UIDimensions, UIShownChangedEventCallback } from './services/ui.service'
|
||||
import { FadeInOutAnimation, FromTopAnimation } from '@eqmac/components'
|
||||
import { MatDialog } from '@angular/material/dialog'
|
||||
import { TransitionService } from './services/transitions.service'
|
||||
import { AnalyticsService } from './services/analytics.service'
|
||||
import { ApplicationService } from './services/app.service'
|
||||
import { SettingsService, IconMode } from './sections/settings/settings.service'
|
||||
import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -34,7 +35,7 @@ export class AppComponent implements OnInit, AfterContentInit {
|
||||
constructor (
|
||||
public utils: UtilitiesService,
|
||||
public ui: UIService,
|
||||
public matDialog: MatDialog,
|
||||
public dialog: MatDialog,
|
||||
public transitions: TransitionService,
|
||||
public analytics: AnalyticsService,
|
||||
public app: ApplicationService,
|
||||
@ -46,10 +47,34 @@ export class AppComponent implements OnInit, AfterContentInit {
|
||||
async ngOnInit () {
|
||||
await this.sync()
|
||||
await this.fixUIMode()
|
||||
this.analytics.send()
|
||||
setInterval(() => {
|
||||
this.analytics.ping()
|
||||
}, 1000 * 60)
|
||||
|
||||
const uiSettings = await this.ui.getSettings()
|
||||
|
||||
if (typeof uiSettings.doCollectTelemetry !== 'boolean') {
|
||||
uiSettings.doCollectTelemetry = await this.dialog.open(ConfirmDialogComponent, {
|
||||
hasBackdrop: true,
|
||||
disableClose: true,
|
||||
data: {
|
||||
text: `Is it okay with you if eqMac will collect anonymous Telemetry analytics data like:
|
||||
|
||||
• macOS Version
|
||||
• App and UI Version
|
||||
• Country (IP Addresses are anonymized)
|
||||
|
||||
This helps us understand distribution of eqMac's users.
|
||||
You can change this setting any time later in the Settings.`,
|
||||
cancelText: 'Don\'t collect',
|
||||
confirmText: 'It\'s okay'
|
||||
}
|
||||
}).afterClosed().toPromise()
|
||||
await this.ui.setSettings({
|
||||
doCollectTelemetry: uiSettings.doCollectTelemetry
|
||||
})
|
||||
}
|
||||
|
||||
if (uiSettings.doCollectTelemetry) {
|
||||
await this.analytics.init()
|
||||
}
|
||||
}
|
||||
|
||||
async ngAfterContentInit () {
|
||||
@ -129,7 +154,7 @@ export class AppComponent implements OnInit, AfterContentInit {
|
||||
|
||||
closeDropdownSection (section: string, event?: any) {
|
||||
// if (event && event.target && ['backdrop', 'mat-dialog'].some(e => event.target.className.includes(e))) return
|
||||
if (this.matDialog.openDialogs.length > 0) return
|
||||
if (this.dialog.openDialogs.length > 0) return
|
||||
if (section in this.showDropdownSections) {
|
||||
this.showDropdownSections[section] = false
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div fxLayout="column" fxLayoutGap="10px">
|
||||
<eqm-label>
|
||||
<eqm-label style="white-space: pre-line;">
|
||||
{{text}}
|
||||
</eqm-label>
|
||||
<ng-content></ng-content>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<div fxLayout="column" fxLayoutAlign="space-around start" fxLayoutGap="10px">
|
||||
<div fxLayout="column" fxLayoutAlign="space-around start" fxLayoutGap="15px">
|
||||
<div *ngFor="let row of options" fxLayout="row" style="width: 100%" fxLayoutGap="10px" fxLayoutAlign="space-between center">
|
||||
<div *ngFor="let option of row" [ngStyle]="getOptionStyle(option, row)">
|
||||
<!-- Checkbox -->
|
||||
<div *ngIf="option.type === 'checkbox'"
|
||||
fxLayout="row" fxLayoutAlign="center center" fxFill fxLayoutGap="10px"
|
||||
class="pointer"
|
||||
[eqmTooltip]="option.tooltip"
|
||||
(click)="toggleCheckbox(option)">
|
||||
<eqm-checkbox [labelSide]="option.label && 'right'" [interactive]="false" [checked]="option.value">{{option.label}}</eqm-checkbox>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ interface BaseOptions {
|
||||
type: string
|
||||
isEnabled?: () => boolean
|
||||
style?: { [style: string]: string | number }
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export interface ButtonOption extends BaseOptions {
|
||||
|
@ -5,6 +5,7 @@ import { ApplicationService } from '../../services/app.service'
|
||||
import { MatDialog } from '@angular/material/dialog'
|
||||
import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'
|
||||
import { UIService } from '../../services/ui.service'
|
||||
import { AnalyticsService } from '../../services/analytics.service'
|
||||
|
||||
@Component({
|
||||
selector: 'eqm-settings',
|
||||
@ -29,6 +30,29 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
doCollectTelemetryOption: CheckboxOption = {
|
||||
type: 'checkbox',
|
||||
label: 'Send Anonymous Analytics data',
|
||||
tooltip: `
|
||||
eqMac will collect anonymous Telemetry analytics data like:
|
||||
|
||||
• macOS Version
|
||||
• App and UI Version
|
||||
• Country (IP Addresses are anonymized)
|
||||
|
||||
This helps us understand distribution of our users.
|
||||
`,
|
||||
value: false,
|
||||
toggled: doCollectTelemetry => {
|
||||
this.ui.setSettings({ doCollectTelemetry })
|
||||
if (doCollectTelemetry) {
|
||||
this.analytics.init()
|
||||
} else {
|
||||
this.analytics.deinit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iconModeOption: SelectOption = {
|
||||
type: 'select',
|
||||
label: 'Show Icon',
|
||||
@ -67,17 +91,25 @@ export class SettingsComponent implements OnInit {
|
||||
|
||||
settings: Options = [
|
||||
[
|
||||
this.updateOption
|
||||
{
|
||||
type: 'label',
|
||||
label: 'Settings'
|
||||
}
|
||||
],
|
||||
[
|
||||
this.iconModeOption
|
||||
],
|
||||
[
|
||||
this.updateOption,
|
||||
this.uninstallOption
|
||||
],
|
||||
|
||||
[
|
||||
this.replaceKnobsWithSlidersOption,
|
||||
this.launchOnStartupOption
|
||||
],
|
||||
[
|
||||
this.uninstallOption
|
||||
this.doCollectTelemetryOption
|
||||
]
|
||||
]
|
||||
|
||||
@ -85,7 +117,8 @@ export class SettingsComponent implements OnInit {
|
||||
public settingsService: SettingsService,
|
||||
public app: ApplicationService,
|
||||
public dialog: MatDialog,
|
||||
public ui: UIService
|
||||
public ui: UIService,
|
||||
public analytics: AnalyticsService
|
||||
) {
|
||||
}
|
||||
|
||||
@ -112,6 +145,7 @@ export class SettingsComponent implements OnInit {
|
||||
this.iconModeOption.selectedId = iconMode
|
||||
this.launchOnStartupOption.value = launchOnStartup
|
||||
this.replaceKnobsWithSlidersOption.value = UISettings.replaceKnobsWithSliders
|
||||
this.doCollectTelemetryOption.value = UISettings.doCollectTelemetry
|
||||
}
|
||||
|
||||
async update () {
|
||||
|
@ -2,6 +2,15 @@ import { Injectable } from '@angular/core'
|
||||
import { UtilitiesService } from './utilities.service'
|
||||
import { ApplicationService } from './app.service'
|
||||
import packageJson from '../../../package.json'
|
||||
import { UIService, UIShownChangedEventCallback } from './ui.service'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
gaData: any
|
||||
gaGlobal: any
|
||||
gaplugins: any
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -9,45 +18,75 @@ import packageJson from '../../../package.json'
|
||||
export class AnalyticsService {
|
||||
constructor (
|
||||
public utils: UtilitiesService,
|
||||
public app: ApplicationService
|
||||
public app: ApplicationService,
|
||||
private readonly ui: UIService
|
||||
) {}
|
||||
|
||||
private _tracker: UniversalAnalytics.Tracker
|
||||
|
||||
public get tracker () {
|
||||
return new Promise<UniversalAnalytics.Tracker>(async (resolve, reject) => {
|
||||
try {
|
||||
if (!this._tracker) {
|
||||
await this.utils.waitForProperty(window, 'ga')
|
||||
await this.utils.delay(1000)
|
||||
await this.utils.waitForProperty(ga, 'getAll')
|
||||
this._tracker = ga.getAll()[0]
|
||||
}
|
||||
resolve(this._tracker)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
private injected = false
|
||||
private readonly SCRIPT_ID = 'google-analytics'
|
||||
private readonly UIIsShownListener: UIShownChangedEventCallback = ({ isShown }) => {
|
||||
if (isShown) {
|
||||
this.ping()
|
||||
}
|
||||
}
|
||||
|
||||
async send () {
|
||||
const [ tracker, info ] = await Promise.all([
|
||||
this.tracker,
|
||||
this.app.getInfo()
|
||||
])
|
||||
async init () {
|
||||
if (this.injected) return
|
||||
|
||||
await UtilitiesService.injectScript({
|
||||
id: this.SCRIPT_ID,
|
||||
src: 'https://www.google-analytics.com/analytics.js'
|
||||
})
|
||||
this.injected = true
|
||||
|
||||
window.ga('create', 'UA-96287398-6')
|
||||
this.send()
|
||||
|
||||
this.clearPingTimer()
|
||||
this.pingTimer = setInterval(() => {
|
||||
this.ping()
|
||||
}, this.pingIntervalMs) as any
|
||||
this.ui.onShownChanged(this.UIIsShownListener)
|
||||
}
|
||||
|
||||
private async send () {
|
||||
const info = await this.app.getInfo()
|
||||
const data = {
|
||||
appName: 'eqMac',
|
||||
appVersion: `${info.version}`,
|
||||
screenName: 'Home',
|
||||
dimension1: `${packageJson.version}`
|
||||
dimension1: `${packageJson.version}`,
|
||||
dimension2: `${info.isOpenSource}`,
|
||||
dimension3: `${this.ui.isRemote}`
|
||||
}
|
||||
tracker.send('screenview', data)
|
||||
window.ga('send', 'screenview', data)
|
||||
}
|
||||
|
||||
async ping () {
|
||||
const tracker = await this.tracker
|
||||
tracker.send('screenview', {
|
||||
private pingTimer: number
|
||||
private readonly pingIntervalMs = 10 * 60 * 1000
|
||||
|
||||
private async ping () {
|
||||
if (!this.injected) return
|
||||
window.ga('send', 'screenview', {
|
||||
screenview: 'Home'
|
||||
})
|
||||
}
|
||||
|
||||
private clearPingTimer () {
|
||||
if (this.pingTimer) {
|
||||
clearInterval(this.pingTimer)
|
||||
this.pingTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
deinit () {
|
||||
this.clearPingTimer()
|
||||
this.ui.offShownChanged(this.UIIsShownListener)
|
||||
window.document.getElementById(this.SCRIPT_ID)?.remove()
|
||||
delete window.ga
|
||||
delete window.gaData
|
||||
delete window.gaGlobal
|
||||
delete window.gaplugins
|
||||
this.injected = false
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppComponent } from '../app.component'
|
||||
import { ConstantsService } from './constants.service'
|
||||
import { DataService } from './data.service'
|
||||
import { ToastService } from './toast.service'
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
@ -16,9 +18,24 @@ export class ApplicationService extends DataService {
|
||||
ref?: AppComponent
|
||||
info?: Info
|
||||
|
||||
constructor (
|
||||
public toast: ToastService,
|
||||
public CONST: ConstantsService
|
||||
) {
|
||||
super()
|
||||
this.on('/error', ({ error }) => {
|
||||
this.toast.show({
|
||||
message: error,
|
||||
type: 'warning'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getInfo (): Promise<Info> {
|
||||
if (!this.info) {
|
||||
this.info = await this.request({ method: 'GET', endpoint: '/info' })
|
||||
// < v1.0.0 didn't return isOpenSource property so need to set it
|
||||
this.info.isOpenSource ??= true
|
||||
}
|
||||
return this.info
|
||||
}
|
||||
@ -32,7 +49,7 @@ export class ApplicationService extends DataService {
|
||||
}
|
||||
|
||||
uninstall () {
|
||||
return this.request({ method: 'GET', endpoint: '/uninstall' })
|
||||
return this.openURL(new URL(`https://${this.CONST.DOMAIN}#uninstall`))
|
||||
}
|
||||
|
||||
haptic () {
|
||||
|
@ -4,6 +4,7 @@ import { Subject } from 'rxjs'
|
||||
|
||||
export interface UISettings {
|
||||
replaceKnobsWithSliders?: boolean
|
||||
doCollectTelemetry?: boolean
|
||||
}
|
||||
|
||||
export interface UIDimensions {
|
||||
@ -88,4 +89,14 @@ export class UIService extends DataService {
|
||||
async loaded () {
|
||||
return this.request({ method: 'POST', endpoint: '/loaded' })
|
||||
}
|
||||
|
||||
onShownChanged (cb: UIShownChangedEventCallback) {
|
||||
this.on('/shown', cb)
|
||||
}
|
||||
|
||||
offShownChanged (cb: UIShownChangedEventCallback) {
|
||||
this.off('/shown', cb)
|
||||
}
|
||||
}
|
||||
|
||||
export type UIShownChangedEventCallback = (data: { isShown: boolean }) => void | Promise<void>
|
||||
|
@ -54,4 +54,24 @@ export class UtilitiesService {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
static async injectScript ({ src, id }: { src: string, id?: string }) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
script.onerror = (err) => {
|
||||
reject(err)
|
||||
}
|
||||
script.src = src
|
||||
if (id) {
|
||||
script.id = id
|
||||
}
|
||||
const head = document.getElementsByTagName('head')[0]
|
||||
head.appendChild(script)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Google Tag Manager -->
|
||||
<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-KBWVP9Q');</script>
|
||||
<!-- End Google Tag Manager -->
|
||||
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
@ -58,11 +50,6 @@
|
||||
<meta name="theme-color" content="#000">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Google Tag Manager (noscript) -->
|
||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-KBWVP9Q"
|
||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||
<!-- End Google Tag Manager (noscript) -->
|
||||
|
||||
<app-root class="app">
|
||||
<div class="loader">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
|
Loading…
Reference in New Issue
Block a user