1
1
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:
Nodeful 2021-07-08 01:26:41 +03:00
parent dc8612c0a3
commit 51ee72a70b
11 changed files with 189 additions and 54 deletions

View File

@ -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",

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -6,6 +6,7 @@ interface BaseOptions {
type: string
isEnabled?: () => boolean
style?: { [style: string]: string | number }
tooltip?: string
}
export interface ButtonOption extends BaseOptions {

View File

@ -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 () {

View File

@ -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
}
}

View File

@ -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 () {

View File

@ -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>

View File

@ -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)
})
}
}

View File

@ -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"