diff --git a/src/App.vue b/src/App.vue index f9aa0400..a557b01f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -84,6 +84,7 @@ export default { TelemetryModal, }, async mounted() { + telemetry.platform = this.platform; const lastSelectedFilePath = config.get('lastSelectedFilePath', null); const { connectionSuccess, reason } = await connectToLocalDatabase( lastSelectedFilePath @@ -132,7 +133,7 @@ export default { }, changeDbFile() { config.set('lastSelectedFilePath', null); - telemetry.stop() + telemetry.stop(); purgeCache(true); this.activeScreen = 'DatabaseSelector'; }, diff --git a/src/initialization.js b/src/initialization.js index d3bfbee8..b486a853 100644 --- a/src/initialization.js +++ b/src/initialization.js @@ -1,11 +1,10 @@ import config from '@/config'; -import { ipcRenderer } from 'electron'; import SQLiteDatabase from 'frappe/backends/sqlite'; import fs from 'fs'; import models from '../models'; import regionalModelUpdates from '../models/regionalModelUpdates'; import postStart, { setCurrencySymbols } from '../server/postStart'; -import { DB_CONN_FAILURE, IPC_ACTIONS } from './messages'; +import { DB_CONN_FAILURE } from './messages'; import runMigrate from './migrate'; import { getId } from './telemetry/helpers'; import telemetry from './telemetry/telemetry'; @@ -97,10 +96,7 @@ export async function connectToLocalDatabase(filePath) { // second init with currency, normal usage await callInitializeMoneyMaker(); - const creds = await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS); - telemetry.setCreds(creds?.telemetryUrl ?? '', creds?.tokenString ?? ''); - telemetry.start(); - await telemetry.setCount(); + await telemetry.start(); if (frappe.store.isDevelopment) { // @ts-ignore diff --git a/src/main.js b/src/main.js index d759ede9..92d91900 100644 --- a/src/main.js +++ b/src/main.js @@ -154,7 +154,12 @@ function registerIpcRendererListeners() { }); document.addEventListener('visibilitychange', function () { - if (document.visibilityState !== 'hidden') { + const { visibilityState } = document; + if (visibilityState === 'visible' && !telemetry.started) { + telemetry.start(); + } + + if (visibilityState !== 'hidden') { return; } diff --git a/src/telemetry/helpers.ts b/src/telemetry/helpers.ts index 1314894d..d25a990f 100644 --- a/src/telemetry/helpers.ts +++ b/src/telemetry/helpers.ts @@ -1,6 +1,8 @@ import config, { ConfigFile, ConfigKeys } from '@/config'; +import { IPC_ACTIONS } from '@/messages'; +import { ipcRenderer } from 'electron'; import { DoctypeName } from '../../models/types'; -import { Count, Locale, UniqueId } from './types'; +import { Count, UniqueId } from './types'; export function getId(): string { let id: string = ''; @@ -12,12 +14,13 @@ export function getId(): string { return id; } -export function getLocale(): Locale { +export function getCountry(): string { // @ts-ignore - const country: string = frappe.AccountingSettings?.country ?? ''; - const language: string = config.get('language') as string; + return frappe.AccountingSettings?.country ?? ''; +} - return { country, language }; +export function getLanguage(): string { + return config.get('language') as string; } export async function getCounts(): Promise { @@ -122,3 +125,10 @@ function setInstanceId(companyName: string, files: ConfigFile[]): UniqueId { config.set(ConfigKeys.Files, files); return id; } + +export async function getCreds() { + const creds = await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS); + const url: string = creds?.telemetryUrl ?? ''; + const token: string = creds?.tokenString ?? ''; + return { url, token }; +} diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index db0c03dc..e2f286e4 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -1,8 +1,48 @@ import config, { ConfigKeys, TelemetrySetting } from '@/config'; import frappe from 'frappe'; import { cloneDeep } from 'lodash'; -import { getCounts, getDeviceId, getInstanceId, getLocale } from './helpers'; -import { Noun, NounEnum, Telemetry, Verb } from './types'; +import { + getCountry, + getCounts, + getCreds, + getDeviceId, + getInstanceId, + getLanguage, +} from './helpers'; +import { Noun, NounEnum, Platform, Telemetry, Verb } from './types'; + +/** + * # Telemetry + * + * ## `start` + * Used to initialize state. It should be called before interaction. + * It is called on three events: + * 1. On db initialization which happens everytime a db is loaded or changed. + * 2. On visibility change if not started, eg: when user minimizeds Books and + * then comes back later. + * 3. When `log` is called if not initialized. + * + * ## `log` + * Used to make entries in the `timeline` which happens only if telmetry + * is set to 'Allow Telemetry` + * + * ## `error` + * Called in errorHandling.ts and maintains a count of errors that were + * thrown during usage. + * + * ## `stop` + * This is to be called when a session is being stopped. It's called on two events + * 1. When the db is being changed. + * 2. When the visiblity has changed which happens when either the app is being shut or + * the app is hidden. + * + * This function can't be async as it's called when visibility changes to 'hidden' + * at which point async doesn't seem to work and hence count is captured on `start()` + * + * ## `finalLogAndStop` + * Called when telemetry is set to "Don't Log Anything" so as to indicate cessation of + * telemetry and not app usage. + */ class TelemetryManager { #url: string = ''; @@ -10,32 +50,42 @@ class TelemetryManager { #started = false; #telemetryObject: Partial = {}; - start() { - this.#telemetryObject.locale = getLocale(); + set platform(value: Platform) { + this.#telemetryObject.platform ||= value; + } + + get hasCreds() { + return !!this.#url && !!this.#token; + } + + get started() { + return this.#started; + } + + get telemetryObject(): Readonly> { + return cloneDeep(this.#telemetryObject); + } + + async start() { + this.#telemetryObject.country ||= getCountry(); + this.#telemetryObject.language ??= getLanguage(); this.#telemetryObject.deviceId ||= getDeviceId(); this.#telemetryObject.instanceId ||= getInstanceId(); this.#telemetryObject.openTime ||= new Date().valueOf(); this.#telemetryObject.timeline ??= []; this.#telemetryObject.errors ??= {}; + this.#telemetryObject.counts ??= {}; this.#started = true; + + await this.#postStart(); } - getCanLog(): boolean { - const telemetrySetting = config.get(ConfigKeys.Telemetry) as string; - return telemetrySetting === TelemetrySetting.allow; - } - - setCreds(url: string, token: string) { - this.#url ||= url; - this.#token ||= token; - } - - log(verb: Verb, noun: Noun, more?: Record) { + async log(verb: Verb, noun: Noun, more?: Record) { if (!this.#started) { - this.start(); + await this.start(); } - if (!this.getCanLog()) { + if (!this.#getCanLog()) { return; } @@ -56,32 +106,23 @@ class TelemetryManager { this.#telemetryObject.errors[name] += 1; } - async setCount() { - this.#telemetryObject.counts = this.getCanLog() ? await getCounts() : {}; - } - stop() { - // Will set ids if not set. - this.start(); + this.#started = false; //@ts-ignore this.#telemetryObject.version = frappe.store.appVersion ?? ''; this.#telemetryObject.closeTime = new Date().valueOf(); - const telemetryObject = this.#telemetryObject; + const data = JSON.stringify({ + token: this.#token, + telemetryData: this.#telemetryObject, + }); - this.#started = false; - this.#telemetryObject = {}; + this.#clear(); if (config.get(ConfigKeys.Telemetry) === TelemetrySetting.dontLogAnything) { return; } - - const data = JSON.stringify({ - token: this.#token, - telemetryData: telemetryObject, - }); - navigator.sendBeacon(this.#url, data); } @@ -90,8 +131,43 @@ class TelemetryManager { this.stop(); } - get telemetryObject(): Readonly> { - return cloneDeep(this.#telemetryObject); + async #postStart() { + await this.#setCount(); + await this.#setCreds(); + } + + async #setCount() { + if (!this.#getCanLog()) { + return; + } + + this.#telemetryObject.counts = await getCounts(); + } + + async #setCreds() { + if (this.hasCreds) { + return; + } + + const { url, token } = await getCreds(); + this.#url = url; + this.#token = token; + } + + #getCanLog(): boolean { + const telemetrySetting = config.get(ConfigKeys.Telemetry) as string; + return telemetrySetting === TelemetrySetting.allow; + } + + #clear() { + // Delete only what varies + delete this.#telemetryObject.openTime; + delete this.#telemetryObject.closeTime; + delete this.#telemetryObject.errors; + delete this.#telemetryObject.counts; + delete this.#telemetryObject.timeline; + delete this.#telemetryObject.instanceId; + delete this.#telemetryObject.country; } } diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index afeffae4..804232e0 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -11,11 +11,6 @@ export interface InteractionEvent { more?: Record; } -export interface Locale { - country: string; - language: string; -} - export type Count = Partial<{ [key in DoctypeName]: number; }>; @@ -30,7 +25,8 @@ export interface Telemetry { timeline?: InteractionEvent[]; counts?: Count; errors: Record; - locale: Locale; + country: string; + language: string; version: AppVersion; }