Skip to content

Commit 51ee72a

Browse files
committed
added analytics/telemetry consent and setting
1 parent dc8612c commit 51ee72a

11 files changed

Lines changed: 188 additions & 53 deletions

File tree

ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ui",
3-
"version": "1.3.4",
3+
"version": "1.4.1",
44
"scripts": {
55
"lint": "npx eslint .",
66
"start": "ng serve --port 8080 --host 0.0.0.0 --disable-host-check",

ui/src/app/app.component.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import {
55
AfterContentInit
66
} from '@angular/core'
77
import { UtilitiesService } from './services/utilities.service'
8-
import { UIService, UIDimensions } from './services/ui.service'
8+
import { UIService, UIDimensions, UIShownChangedEventCallback } from './services/ui.service'
99
import { FadeInOutAnimation, FromTopAnimation } from '@eqmac/components'
1010
import { MatDialog } from '@angular/material/dialog'
1111
import { TransitionService } from './services/transitions.service'
1212
import { AnalyticsService } from './services/analytics.service'
1313
import { ApplicationService } from './services/app.service'
1414
import { SettingsService, IconMode } from './sections/settings/settings.service'
15+
import { ConfirmDialogComponent } from './components/confirm-dialog/confirm-dialog.component'
1516

1617
@Component({
1718
selector: 'app-root',
@@ -34,7 +35,7 @@ export class AppComponent implements OnInit, AfterContentInit {
3435
constructor (
3536
public utils: UtilitiesService,
3637
public ui: UIService,
37-
public matDialog: MatDialog,
38+
public dialog: MatDialog,
3839
public transitions: TransitionService,
3940
public analytics: AnalyticsService,
4041
public app: ApplicationService,
@@ -46,10 +47,34 @@ export class AppComponent implements OnInit, AfterContentInit {
4647
async ngOnInit () {
4748
await this.sync()
4849
await this.fixUIMode()
49-
this.analytics.send()
50-
setInterval(() => {
51-
this.analytics.ping()
52-
}, 1000 * 60)
50+
51+
const uiSettings = await this.ui.getSettings()
52+
53+
if (typeof uiSettings.doCollectTelemetry !== 'boolean') {
54+
uiSettings.doCollectTelemetry = await this.dialog.open(ConfirmDialogComponent, {
55+
hasBackdrop: true,
56+
disableClose: true,
57+
data: {
58+
text: `Is it okay with you if eqMac will collect anonymous Telemetry analytics data like:
59+
60+
• macOS Version
61+
• App and UI Version
62+
• Country (IP Addresses are anonymized)
63+
64+
This helps us understand distribution of eqMac's users.
65+
You can change this setting any time later in the Settings.`,
66+
cancelText: 'Don\'t collect',
67+
confirmText: 'It\'s okay'
68+
}
69+
}).afterClosed().toPromise()
70+
await this.ui.setSettings({
71+
doCollectTelemetry: uiSettings.doCollectTelemetry
72+
})
73+
}
74+
75+
if (uiSettings.doCollectTelemetry) {
76+
await this.analytics.init()
77+
}
5378
}
5479

5580
async ngAfterContentInit () {
@@ -129,7 +154,7 @@ export class AppComponent implements OnInit, AfterContentInit {
129154

130155
closeDropdownSection (section: string, event?: any) {
131156
// if (event && event.target && ['backdrop', 'mat-dialog'].some(e => event.target.className.includes(e))) return
132-
if (this.matDialog.openDialogs.length > 0) return
157+
if (this.dialog.openDialogs.length > 0) return
133158
if (section in this.showDropdownSections) {
134159
this.showDropdownSections[section] = false
135160
}

ui/src/app/components/confirm-dialog/confirm-dialog.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div fxLayout="column" fxLayoutGap="10px">
2-
<eqm-label>
2+
<eqm-label style="white-space: pre-line;">
33
{{text}}
44
</eqm-label>
55
<ng-content></ng-content>

ui/src/app/components/options/options.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
<div fxLayout="column" fxLayoutAlign="space-around start" fxLayoutGap="10px">
1+
<div fxLayout="column" fxLayoutAlign="space-around start" fxLayoutGap="15px">
22
<div *ngFor="let row of options" fxLayout="row" style="width: 100%" fxLayoutGap="10px" fxLayoutAlign="space-between center">
33
<div *ngFor="let option of row" [ngStyle]="getOptionStyle(option, row)">
44
<!-- Checkbox -->
55
<div *ngIf="option.type === 'checkbox'"
66
fxLayout="row" fxLayoutAlign="center center" fxFill fxLayoutGap="10px"
77
class="pointer"
8+
[eqmTooltip]="option.tooltip"
89
(click)="toggleCheckbox(option)">
910
<eqm-checkbox [labelSide]="option.label && 'right'" [interactive]="false" [checked]="option.value">{{option.label}}</eqm-checkbox>
1011
</div>

ui/src/app/components/options/options.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface BaseOptions {
66
type: string
77
isEnabled?: () => boolean
88
style?: { [style: string]: string | number }
9+
tooltip?: string
910
}
1011

1112
export interface ButtonOption extends BaseOptions {

ui/src/app/sections/settings/settings.component.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ApplicationService } from '../../services/app.service'
55
import { MatDialog } from '@angular/material/dialog'
66
import { ConfirmDialogComponent } from '../../components/confirm-dialog/confirm-dialog.component'
77
import { UIService } from '../../services/ui.service'
8+
import { AnalyticsService } from '../../services/analytics.service'
89

910
@Component({
1011
selector: 'eqm-settings',
@@ -29,6 +30,29 @@ export class SettingsComponent implements OnInit {
2930
}
3031
}
3132

33+
doCollectTelemetryOption: CheckboxOption = {
34+
type: 'checkbox',
35+
label: 'Send Anonymous Analytics data',
36+
tooltip: `
37+
eqMac will collect anonymous Telemetry analytics data like:
38+
39+
• macOS Version
40+
• App and UI Version
41+
• Country (IP Addresses are anonymized)
42+
43+
This helps us understand distribution of our users.
44+
`,
45+
value: false,
46+
toggled: doCollectTelemetry => {
47+
this.ui.setSettings({ doCollectTelemetry })
48+
if (doCollectTelemetry) {
49+
this.analytics.init()
50+
} else {
51+
this.analytics.deinit()
52+
}
53+
}
54+
}
55+
3256
iconModeOption: SelectOption = {
3357
type: 'select',
3458
label: 'Show Icon',
@@ -67,25 +91,34 @@ export class SettingsComponent implements OnInit {
6791

6892
settings: Options = [
6993
[
70-
this.updateOption
94+
{
95+
type: 'label',
96+
label: 'Settings'
97+
}
7198
],
7299
[
73100
this.iconModeOption
74101
],
102+
[
103+
this.updateOption,
104+
this.uninstallOption
105+
],
106+
75107
[
76108
this.replaceKnobsWithSlidersOption,
77109
this.launchOnStartupOption
78110
],
79111
[
80-
this.uninstallOption
112+
this.doCollectTelemetryOption
81113
]
82114
]
83115

84116
constructor (
85117
public settingsService: SettingsService,
86118
public app: ApplicationService,
87119
public dialog: MatDialog,
88-
public ui: UIService
120+
public ui: UIService,
121+
public analytics: AnalyticsService
89122
) {
90123
}
91124

@@ -112,6 +145,7 @@ export class SettingsComponent implements OnInit {
112145
this.iconModeOption.selectedId = iconMode
113146
this.launchOnStartupOption.value = launchOnStartup
114147
this.replaceKnobsWithSlidersOption.value = UISettings.replaceKnobsWithSliders
148+
this.doCollectTelemetryOption.value = UISettings.doCollectTelemetry
115149
}
116150

117151
async update () {

ui/src/app/services/analytics.service.ts

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,91 @@ import { Injectable } from '@angular/core'
22
import { UtilitiesService } from './utilities.service'
33
import { ApplicationService } from './app.service'
44
import packageJson from '../../../package.json'
5+
import { UIService, UIShownChangedEventCallback } from './ui.service'
6+
7+
declare global {
8+
interface Window {
9+
gaData: any
10+
gaGlobal: any
11+
gaplugins: any
12+
}
13+
}
514

615
@Injectable({
716
providedIn: 'root'
817
})
918
export class AnalyticsService {
1019
constructor (
1120
public utils: UtilitiesService,
12-
public app: ApplicationService
21+
public app: ApplicationService,
22+
private readonly ui: UIService
1323
) {}
1424

15-
private _tracker: UniversalAnalytics.Tracker
16-
17-
public get tracker () {
18-
return new Promise<UniversalAnalytics.Tracker>(async (resolve, reject) => {
19-
try {
20-
if (!this._tracker) {
21-
await this.utils.waitForProperty(window, 'ga')
22-
await this.utils.delay(1000)
23-
await this.utils.waitForProperty(ga, 'getAll')
24-
this._tracker = ga.getAll()[0]
25-
}
26-
resolve(this._tracker)
27-
} catch (err) {
28-
reject(err)
29-
}
25+
private injected = false
26+
private readonly SCRIPT_ID = 'google-analytics'
27+
private readonly UIIsShownListener: UIShownChangedEventCallback = ({ isShown }) => {
28+
if (isShown) {
29+
this.ping()
30+
}
31+
}
32+
33+
async init () {
34+
if (this.injected) return
35+
36+
await UtilitiesService.injectScript({
37+
id: this.SCRIPT_ID,
38+
src: 'https://www.google-analytics.com/analytics.js'
3039
})
40+
this.injected = true
41+
42+
window.ga('create', 'UA-96287398-6')
43+
this.send()
44+
45+
this.clearPingTimer()
46+
this.pingTimer = setInterval(() => {
47+
this.ping()
48+
}, this.pingIntervalMs) as any
49+
this.ui.onShownChanged(this.UIIsShownListener)
3150
}
3251

33-
async send () {
34-
const [ tracker, info ] = await Promise.all([
35-
this.tracker,
36-
this.app.getInfo()
37-
])
52+
private async send () {
53+
const info = await this.app.getInfo()
3854
const data = {
3955
appName: 'eqMac',
4056
appVersion: `${info.version}`,
4157
screenName: 'Home',
42-
dimension1: `${packageJson.version}`
58+
dimension1: `${packageJson.version}`,
59+
dimension2: `${info.isOpenSource}`,
60+
dimension3: `${this.ui.isRemote}`
4361
}
44-
tracker.send('screenview', data)
62+
window.ga('send', 'screenview', data)
4563
}
4664

47-
async ping () {
48-
const tracker = await this.tracker
49-
tracker.send('screenview', {
65+
private pingTimer: number
66+
private readonly pingIntervalMs = 10 * 60 * 1000
67+
68+
private async ping () {
69+
if (!this.injected) return
70+
window.ga('send', 'screenview', {
5071
screenview: 'Home'
5172
})
5273
}
74+
75+
private clearPingTimer () {
76+
if (this.pingTimer) {
77+
clearInterval(this.pingTimer)
78+
this.pingTimer = undefined
79+
}
80+
}
81+
82+
deinit () {
83+
this.clearPingTimer()
84+
this.ui.offShownChanged(this.UIIsShownListener)
85+
window.document.getElementById(this.SCRIPT_ID)?.remove()
86+
delete window.ga
87+
delete window.gaData
88+
delete window.gaGlobal
89+
delete window.gaplugins
90+
this.injected = false
91+
}
5392
}

ui/src/app/services/app.service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Injectable } from '@angular/core'
22
import { AppComponent } from '../app.component'
3+
import { ConstantsService } from './constants.service'
34
import { DataService } from './data.service'
5+
import { ToastService } from './toast.service'
46

57
export interface Info {
68
name: string
@@ -16,9 +18,24 @@ export class ApplicationService extends DataService {
1618
ref?: AppComponent
1719
info?: Info
1820

21+
constructor (
22+
public toast: ToastService,
23+
public CONST: ConstantsService
24+
) {
25+
super()
26+
this.on('/error', ({ error }) => {
27+
this.toast.show({
28+
message: error,
29+
type: 'warning'
30+
})
31+
})
32+
}
33+
1934
async getInfo (): Promise<Info> {
2035
if (!this.info) {
2136
this.info = await this.request({ method: 'GET', endpoint: '/info' })
37+
// < v1.0.0 didn't return isOpenSource property so need to set it
38+
this.info.isOpenSource ??= true
2239
}
2340
return this.info
2441
}
@@ -32,7 +49,7 @@ export class ApplicationService extends DataService {
3249
}
3350

3451
uninstall () {
35-
return this.request({ method: 'GET', endpoint: '/uninstall' })
52+
return this.openURL(new URL(`https://${this.CONST.DOMAIN}#uninstall`))
3653
}
3754

3855
haptic () {

ui/src/app/services/ui.service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Subject } from 'rxjs'
44

55
export interface UISettings {
66
replaceKnobsWithSliders?: boolean
7+
doCollectTelemetry?: boolean
78
}
89

910
export interface UIDimensions {
@@ -88,4 +89,14 @@ export class UIService extends DataService {
8889
async loaded () {
8990
return this.request({ method: 'POST', endpoint: '/loaded' })
9091
}
92+
93+
onShownChanged (cb: UIShownChangedEventCallback) {
94+
this.on('/shown', cb)
95+
}
96+
97+
offShownChanged (cb: UIShownChangedEventCallback) {
98+
this.off('/shown', cb)
99+
}
91100
}
101+
102+
export type UIShownChangedEventCallback = (data: { isShown: boolean }) => void | Promise<void>

0 commit comments

Comments
 (0)