2
0
mirror of https://github.com/frappe/books.git synced 2025-01-08 17:24:05 +00:00

incr: simplify telemetry

- it's not opt-in anymore cause my job deps on it
This commit is contained in:
18alantom 2022-05-23 13:39:07 +05:30
parent deefcd08f9
commit 2c596b11c8
18 changed files with 224 additions and 491 deletions

View File

@ -48,7 +48,12 @@ export async function setupDummyInstance(
notifier?.(fyo.t`Creating Items and Parties`, -1); notifier?.(fyo.t`Creating Items and Parties`, -1);
await generateStaticEntries(fyo); await generateStaticEntries(fyo);
await generateDynamicEntries(fyo, years, baseCount, notifier); await generateDynamicEntries(fyo, years, baseCount, notifier);
return options.companyName;
const instanceId = (await fyo.getValue(
ModelNameEnum.SystemSettings,
'instanceId'
)) as string;
return { companyName: options.companyName, instanceId };
} }
/** /**

View File

@ -34,7 +34,6 @@ export enum ConfigKeys {
LastSelectedFilePath = 'lastSelectedFilePath', LastSelectedFilePath = 'lastSelectedFilePath',
Language = 'language', Language = 'language',
DeviceId = 'deviceId', DeviceId = 'deviceId',
Telemetry = 'telemetry',
} }
export interface ConfigFile { export interface ConfigFile {

View File

@ -678,6 +678,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this.setAndSync('submitted', true); await this.setAndSync('submitted', true);
await this.trigger('afterSubmit'); await this.trigger('afterSubmit');
this.fyo.telemetry.log(Verb.Submitted, this.schemaName);
this.fyo.doc.observer.trigger(`submit:${this.schemaName}`, this.name); this.fyo.doc.observer.trigger(`submit:${this.schemaName}`, this.name);
} }
@ -690,6 +691,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this.setAndSync('cancelled', true); await this.setAndSync('cancelled', true);
await this.trigger('afterCancel'); await this.trigger('afterCancel');
this.fyo.telemetry.log(Verb.Cancelled, this.schemaName);
this.fyo.doc.observer.trigger(`cancel:${this.schemaName}`, this.name); this.fyo.doc.observer.trigger(`cancel:${this.schemaName}`, this.name);
} }

View File

@ -1,18 +1,9 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { ConfigFile, ConfigKeys } from 'fyo/core/types'; import { ConfigFile, ConfigKeys } from 'fyo/core/types';
import { DEFAULT_COUNTRY_CODE } from 'fyo/utils/consts'; import { DEFAULT_COUNTRY_CODE } from 'fyo/utils/consts';
import { t } from 'fyo/utils/translation'; import { ModelNameEnum } from 'models/types';
import { Count, TelemetrySetting, UniqueId } from './types'; import { getRandomString } from 'utils';
import { UniqueId } from './types';
export function getId(): string {
let id: string = '';
for (let i = 0; i < 4; i++) {
id += Math.random().toString(36).slice(2, 9);
}
return id;
}
export function getCountry(fyo: Fyo): string { export function getCountry(fyo: Fyo): string {
return ( return (
@ -24,27 +15,10 @@ export function getLanguage(fyo: Fyo): string {
return fyo.config.get('language') as string; return fyo.config.get('language') as string;
} }
export async function getCounts(
interestingDocs: string[],
fyo: Fyo
): Promise<Count> {
const countMap: Count = {};
if (fyo.db === undefined) {
return countMap;
}
for (const name of interestingDocs) {
const count: number = (await fyo.db.getAll(name)).length;
countMap[name] = count;
}
return countMap;
}
export function getDeviceId(fyo: Fyo): UniqueId { export function getDeviceId(fyo: Fyo): UniqueId {
let deviceId = fyo.config.get(ConfigKeys.DeviceId) as string | undefined; let deviceId = fyo.config.get(ConfigKeys.DeviceId) as string | undefined;
if (deviceId === undefined) { if (deviceId === undefined) {
deviceId = getId(); deviceId = getRandomString();
fyo.config.set(ConfigKeys.DeviceId, deviceId); fyo.config.set(ConfigKeys.DeviceId, deviceId);
} }
@ -53,69 +27,69 @@ export function getDeviceId(fyo: Fyo): UniqueId {
export function getInstanceId(fyo: Fyo): UniqueId { export function getInstanceId(fyo: Fyo): UniqueId {
const files = (fyo.config.get(ConfigKeys.Files) ?? []) as ConfigFile[]; const files = (fyo.config.get(ConfigKeys.Files) ?? []) as ConfigFile[];
const instanceId = fyo.singles.SystemSettings!.instanceId as string;
const dbPath = fyo.db.dbPath!;
const companyName = fyo.singles.AccountingSettings!.companyName as string;
const companyName = fyo.singles.AccountingSettings?.companyName as string; let file = files.find((f) => f.id === instanceId);
if (companyName === undefined) {
return '';
}
const file = files.find((f) => f.companyName === companyName);
if (file === undefined) { if (file === undefined) {
return addNewFile(companyName, fyo, files); file = addNewConfigFile(companyName, dbPath, instanceId, files, fyo);
} }
if (file.id === undefined) { if (!file.id) {
return setInstanceId(companyName, files, fyo); setIdOnConfigFile(instanceId, companyName, dbPath, files, fyo);
} }
return file.id; return instanceId;
} }
export function addNewFile( export function addNewConfigFile(
companyName: string, companyName: string,
fyo: Fyo, dbPath: string,
files?: ConfigFile[], instanceId: string,
dbPath?: string files: ConfigFile[],
): UniqueId { fyo: Fyo
files ??= fyo.config.get(ConfigKeys.Files, []) as ConfigFile[]; ): ConfigFile {
dbPath ??= fyo.config.get(ConfigKeys.LastSelectedFilePath, '') as string;
const newFile: ConfigFile = { const newFile: ConfigFile = {
companyName, companyName,
dbPath, dbPath,
id: getId(), id: instanceId,
openCount: 0, openCount: 0,
}; };
files.push(newFile); files.push(newFile);
fyo.config.set(ConfigKeys.Files, files); fyo.config.set(ConfigKeys.Files, files);
return newFile.id; return newFile;
} }
function setInstanceId( export async function getVersion(fyo: Fyo) {
const version = (await fyo.getValue(
ModelNameEnum.SystemSettings,
'version'
)) as string | undefined;
if (version) {
return version;
}
return fyo.store.appVersion;
}
function setIdOnConfigFile(
instanceId: string,
companyName: string, companyName: string,
dbPath: string,
files: ConfigFile[], files: ConfigFile[],
fyo: Fyo fyo: Fyo
): UniqueId { ) {
let id = '';
for (const file of files) { for (const file of files) {
if (file.id) { if (file.companyName !== companyName || file.dbPath !== dbPath) {
continue; continue;
} }
file.id = getId(); file.id = instanceId;
if (file.companyName === companyName) {
id = file.id;
}
} }
fyo.config.set(ConfigKeys.Files, files); fyo.config.set(ConfigKeys.Files, files);
return id;
} }
export const getTelemetryOptions = () => ({
[TelemetrySetting.allow]: t`Allow Telemetry`,
[TelemetrySetting.dontLogUsage]: t`Don't Log Usage`,
[TelemetrySetting.dontLogAnything]: t`Don't Log Anything`,
});

View File

@ -1,53 +1,38 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { ConfigKeys } from 'fyo/core/types';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { DateTime } from 'luxon';
import { import {
getCountry, getCountry,
getCounts,
getDeviceId, getDeviceId,
getInstanceId, getInstanceId,
getLanguage, getLanguage,
getVersion,
} from './helpers'; } from './helpers';
import { import { Noun, Platform, Telemetry, Verb } from './types';
Noun,
NounEnum,
Platform,
Telemetry,
TelemetrySetting,
Verb,
} from './types';
/** /**
* # Telemetry * # Telemetry
* Used to check if people are using Books or not. All logging
* happens using navigator.sendBeacon
* *
* ## `start` * ## `start`
* Used to initialize state. It should be called before interaction. * Used to initialize state. It should be called before any logging and after an
* instance has loaded.
* It is called on three events: * It is called on three events:
* 1. On db initialization which happens everytime a db is loaded or changed. * 1. When Desk is opened, i.e. when the usage starts, this also sends a started
* log.
* 2. On visibility change if not started, eg: when user minimizeds Books and * 2. On visibility change if not started, eg: when user minimizeds Books and
* then comes back later. * then comes back later.
* 3. When `log` is called if not initialized. * 3. When `log` is called, but telemetry wasn't initialized.
* *
* ## `log` * ## `log`
* Used to make entries in the `timeline` which happens only if telmetry * Used to log activity.
* is set to 'Allow Telemetry`
*
* ## `error`
* Called in errorHandling.ts and maintains a count of errors that were
* thrown during usage.
* *
* ## `stop` * ## `stop`
* This is to be called when a session is being stopped. It's called on two events * This is to be called when a session is being stopped. It's called on two events
* 1. When the db is being changed. * 1. When the db is being changed.
* 2. When the visiblity has changed which happens when either the app is being shut or * 2. When the visiblity has changed which happens when either the app is being shut or
* the app is hidden. * 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.
*/ */
export class TelemetryManager { export class TelemetryManager {
@ -55,7 +40,6 @@ export class TelemetryManager {
#token: string = ''; #token: string = '';
#started = false; #started = false;
#telemetryObject: Partial<Telemetry> = {}; #telemetryObject: Partial<Telemetry> = {};
#interestingDocs: string[] = [];
fyo: Fyo; fyo: Fyo;
constructor(fyo: Fyo) { constructor(fyo: Fyo) {
@ -66,10 +50,6 @@ export class TelemetryManager {
this.#telemetryObject.platform ||= value; this.#telemetryObject.platform ||= value;
} }
set interestingDocs(schemaNames: string[]) {
this.#interestingDocs = schemaNames;
}
get hasCreds() { get hasCreds() {
return !!this.#url && !!this.#token; return !!this.#url && !!this.#token;
} }
@ -82,89 +62,57 @@ export class TelemetryManager {
return cloneDeep(this.#telemetryObject); return cloneDeep(this.#telemetryObject);
} }
async start() { async start(openCount?: number) {
this.#telemetryObject.country ||= getCountry(this.fyo); this.#telemetryObject.country ||= getCountry(this.fyo);
this.#telemetryObject.language ??= getLanguage(this.fyo); this.#telemetryObject.language ??= getLanguage(this.fyo);
this.#telemetryObject.deviceId ||= getDeviceId(this.fyo); this.#telemetryObject.device ||= getDeviceId(this.fyo);
this.#telemetryObject.instanceId ||= getInstanceId(this.fyo); this.#telemetryObject.instance ||= getInstanceId(this.fyo);
this.#telemetryObject.openTime ||= new Date().valueOf(); this.#telemetryObject.version ||= await getVersion(this.fyo);
this.#telemetryObject.timeline ??= [];
this.#telemetryObject.errors ??= {};
this.#telemetryObject.counts ??= {};
this.#started = true; this.#started = true;
await this.#setCreds();
await this.#postStart(); if (typeof openCount === 'number') {
} this.#telemetryObject.openCount = openCount;
this.log(Verb.Started, 'telemetry');
async log(verb: Verb, noun: Noun, more?: Record<string, unknown>) { } else {
if (!this.#started) { this.log(Verb.Resumed, 'telemetry');
await this.start();
} }
if (!this.#getCanLog()) {
return;
}
const time = new Date().valueOf();
if (this.#telemetryObject.timeline === undefined) {
this.#telemetryObject.timeline = [];
}
this.#telemetryObject.timeline.push({ time, verb, noun, more });
}
error(name: string) {
if (this.#telemetryObject.errors === undefined) {
this.#telemetryObject.errors = {};
}
this.#telemetryObject.errors[name] ??= 0;
this.#telemetryObject.errors[name] += 1;
} }
stop() { stop() {
if (!this.started) {
return;
}
this.log(Verb.Stopped, 'telemetry');
this.#started = false; this.#started = false;
this.#clear();
}
this.#telemetryObject.version = this.fyo.store.appVersion ?? ''; log(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
this.#telemetryObject.closeTime = new Date().valueOf(); if (!this.#started) {
this.start().then(() => this.#sendBeacon(verb, noun, more));
return;
}
this.#sendBeacon(verb, noun, more);
}
#sendBeacon(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
if (!this.hasCreds) {
return;
}
const telemetryData: Telemetry = this.#getTelemtryData(verb, noun, more);
const data = JSON.stringify({ const data = JSON.stringify({
token: this.#token, token: this.#token,
telemetryData: this.#telemetryObject, telemetryData,
}); });
this.#clear();
if (
this.fyo.config.get(ConfigKeys.Telemetry) ===
TelemetrySetting.dontLogAnything
) {
return;
}
navigator.sendBeacon(this.#url, data); navigator.sendBeacon(this.#url, data);
} }
finalLogAndStop() {
this.log(Verb.Stopped, NounEnum.Telemetry);
this.stop();
}
async #postStart() {
await this.#setCount();
await this.#setCreds();
}
async #setCount() {
if (!this.#getCanLog()) {
return;
}
this.#telemetryObject.counts = await getCounts(
this.#interestingDocs,
this.fyo
);
}
async #setCreds() { async #setCreds() {
if (this.hasCreds) { if (this.hasCreds) {
return; return;
@ -175,21 +123,31 @@ export class TelemetryManager {
this.#token = token; this.#token = token;
} }
#getCanLog(): boolean { #getTelemtryData(
const telemetrySetting = this.fyo.config.get( verb: Verb,
ConfigKeys.Telemetry noun: Noun,
) as string; more?: Record<string, unknown>
return telemetrySetting === TelemetrySetting.allow; ): Telemetry {
return {
country: this.#telemetryObject.country!,
language: this.#telemetryObject.language!,
device: this.#telemetryObject.device!,
instance: this.#telemetryObject.instance!,
version: this.#telemetryObject.version!,
openCount: this.#telemetryObject.openCount!,
timestamp: DateTime.now().toMillis().toString(),
verb,
noun,
more,
};
} }
#clear() { #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; delete this.#telemetryObject.country;
delete this.#telemetryObject.language;
delete this.#telemetryObject.device;
delete this.#telemetryObject.instance;
delete this.#telemetryObject.version;
delete this.#telemetryObject.openCount;
} }
} }

View File

@ -1,50 +1,33 @@
export type AppVersion = string; export type AppVersion = string;
export type UniqueId = string; export type UniqueId = string;
export type Timestamp = number; export type Timestamp = string;
export interface InteractionEvent {
time: Timestamp;
verb: Verb;
noun: Noun;
more?: Record<string, unknown>;
}
export type Count = Record<string, number>;
export type Platform = 'Windows' | 'Mac' | 'Linux'; export type Platform = 'Windows' | 'Mac' | 'Linux';
export interface Telemetry {
deviceId: UniqueId;
instanceId: UniqueId;
openTime: Timestamp;
platform?: Platform;
closeTime: Timestamp;
timeline?: InteractionEvent[];
counts?: Count;
errors: Record<string, number>;
country: string;
language: string;
version: AppVersion;
}
export enum Verb { export enum Verb {
Created = 'created', Created = 'created',
Deleted = 'deleted', Deleted = 'deleted',
Navigated = 'navigated', Submitted = 'submitted',
Cancelled = 'cancelled',
Imported = 'imported', Imported = 'imported',
Exported = 'exported', Exported = 'exported',
Stopped = 'stopped', Stopped = 'stopped',
Started = 'stopped', Started = 'started',
Resumed = 'resumed',
} }
export enum NounEnum { export type Noun = string;
Route = 'route',
Telemetry = 'telemetry',
}
export type Noun = string | NounEnum; export interface Telemetry {
device: UniqueId;
export enum TelemetrySetting { instance: UniqueId;
allow = 'allow', platform?: Platform;
dontLogUsage = 'dontLogUsage', country: string;
dontLogAnything = 'dontLogAnything', language: string;
version: AppVersion;
timestamp: Timestamp;
openCount: number;
verb: Verb;
noun: Noun;
more?: Record<string, unknown>
} }

View File

@ -95,6 +95,12 @@
"label": "Version", "label": "Version",
"fieldtype": "Data", "fieldtype": "Data",
"readOnly": true "readOnly": true
},
{
"fieldname": "instanceId",
"label": "Instance Id",
"fieldtype": "Data",
"readOnly": true
} }
], ],
"quickEditFields": [ "quickEditFields": [

View File

@ -29,20 +29,12 @@
> >
<div id="toast-target" /> <div id="toast-target" />
</div> </div>
<!-- Prompt to Set Telemetry -->
<TelemetryModal />
</div> </div>
</template> </template>
<script> <script>
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { import { getSetupComplete, incrementOpenCount } from 'src/utils/misc';
getSetupComplete,
incrementOpenCount,
startTelemetry
} from 'src/utils/misc';
import TelemetryModal from './components/once/TelemetryModal.vue';
import WindowsTitleBar from './components/WindowsTitleBar.vue'; import WindowsTitleBar from './components/WindowsTitleBar.vue';
import { fyo, initializeInstance } from './initFyo'; import { fyo, initializeInstance } from './initFyo';
import DatabaseSelector from './pages/DatabaseSelector.vue'; import DatabaseSelector from './pages/DatabaseSelector.vue';
@ -65,7 +57,6 @@ export default {
SetupWizard, SetupWizard,
DatabaseSelector, DatabaseSelector,
WindowsTitleBar, WindowsTitleBar,
TelemetryModal,
}, },
async mounted() { async mounted() {
fyo.telemetry.platform = this.platform; fyo.telemetry.platform = this.platform;
@ -86,8 +77,8 @@ export default {
async setDesk(filePath) { async setDesk(filePath) {
this.activeScreen = 'Desk'; this.activeScreen = 'Desk';
await this.setDeskRoute(); await this.setDeskRoute();
await incrementOpenCount(filePath); const openCount = await incrementOpenCount(filePath);
await startTelemetry(); await fyo.telemetry.start(openCount);
await checkForUpdates(false); await checkForUpdates(false);
}, },
async fileSelected(filePath, isNew) { async fileSelected(filePath, isNew) {

View File

@ -1,135 +0,0 @@
<template>
<Modal :open-modal="shouldOpen" class="p-6 flex flex-col gap-3 text-gray-900">
<div class="flex justify-between">
<h1 class="font-bold text-md">{{ t`Set Anonymized Telemetry` }}</h1>
<button @click="shouldOpen = false">
<FeatherIcon name="x" class="w-5 h-5 text-gray-600" />
</button>
</div>
<p class="text-base mt-4">
{{ t`Hello there! 👋` }}
</p>
<p class="text-base">
{{
t`Frappe Books uses opt-in telemetry. This is the only way for us to know if we have any consistent
users. It will be really helpful if you switch it on, but we won't force you. 🙂`
}}
</p>
<p class="text-base mt-4">
{{ t`Please select an option:` }}
</p>
<FormControl
:df="df"
class="text-sm border rounded-md"
@change="
(v) => {
value = v;
}
"
:value="value"
/>
<p class="text-base text-gray-800">{{ description }}</p>
<div class="flex flex-row w-full justify-between items-center mt-12">
<HowTo
link="https://github.com/frappe/books/wiki/Anonymized-Opt-In-Telemetry"
class="text-sm hover:text-gray-900 text-gray-800 py-1 justify-between"
:icon="false"
>{{ t`Know More` }}</HowTo
>
<Button
class="text-sm w-32"
type="primary"
:disabled="!isSet"
@click="saveClicked"
>{{ t`Save Option` }}</Button
>
</div>
</Modal>
</template>
<script>
import { ConfigKeys } from 'fyo/core/types';
import { getTelemetryOptions } from 'fyo/telemetry/helpers';
import { TelemetrySetting } from 'fyo/telemetry/types';
import { fyo } from 'src/initFyo';
import Button from '../Button.vue';
import FormControl from '../Controls/FormControl.vue';
import FeatherIcon from '../FeatherIcon.vue';
import HowTo from '../HowTo.vue';
import Modal from '../Modal.vue';
export default {
components: { Modal, FormControl, Button, HowTo, FeatherIcon },
data() {
return {
shouldOpen: false,
value: '',
};
},
computed: {
df() {
const telemetryOptions = getTelemetryOptions();
return {
fieldname: 'anonymizedTelemetry',
label: this.t`Anonymized Telemetry`,
fieldtype: 'Select',
options: Object.keys(telemetryOptions),
map: telemetryOptions,
default: 'allow',
description: this
.t`Send anonymized usage data and error reports to help improve the product.`,
};
},
description() {
if (!this.isSet) {
return '';
}
return {
[TelemetrySetting.allow]: this
.t`Enables telemetry. Includes usage patterns.`,
[TelemetrySetting.dontLogUsage]: this
.t`Enables telemetry. Does not include usage patterns.`,
[TelemetrySetting.dontLogAnything]: this
.t`Disables telemetry. No data will be collected, you are completely invisble to us.`,
}[this.value];
},
isSet() {
return this.getIsSet(this.value);
},
},
methods: {
saveClicked() {
if (this.value === TelemetrySetting.dontLogUsage) {
telemetry.finalLogAndStop();
} else {
telemetry.log(Verb.Started, NounEnum.Telemetry);
}
config.set(ConfigKeys.Telemetry, this.value);
this.shouldOpen = false;
},
getIsSet(value) {
return [
TelemetrySetting.allow,
TelemetrySetting.dontLogAnything,
TelemetrySetting.dontLogUsage,
].includes(value);
},
setOpen(telemetry) {
const openCount = fyo.config
.get(ConfigKeys.Files)
.map((f) => f.openCount)
.reduce((a, b) => (a ?? 0) + (b ?? 0));
this.shouldOpen = !this.getIsSet(telemetry) && openCount >= 4;
},
},
mounted() {
const telemetry = fyo.config.get(ConfigKeys.Telemetry);
this.setOpen(telemetry);
this.value = telemetry;
},
};
</script>

View File

@ -1,9 +1,11 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { t } from 'fyo'; import { t } from 'fyo';
import { ConfigKeys } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { TelemetrySetting } from 'fyo/telemetry/types'; import {
import { MandatoryError, ValidationError } from 'fyo/utils/errors'; MandatoryError,
NotFoundError,
ValidationError,
} from 'fyo/utils/errors';
import { ErrorLog } from 'fyo/utils/types'; import { ErrorLog } from 'fyo/utils/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { fyo } from './initFyo'; import { fyo } from './initFyo';
@ -11,13 +13,8 @@ import { getErrorMessage } from './utils';
import { ToastOptions } from './utils/types'; import { ToastOptions } from './utils/types';
import { showMessageDialog, showToast } from './utils/ui'; import { showMessageDialog, showToast } from './utils/ui';
function getCanLog(): boolean {
const telemetrySetting = fyo.config.get(ConfigKeys.Telemetry);
return telemetrySetting !== TelemetrySetting.dontLogAnything;
}
function shouldNotStore(error: Error) { function shouldNotStore(error: Error) {
return [MandatoryError, ValidationError].some( return [MandatoryError, ValidationError, NotFoundError].some(
(errorClass) => error instanceof errorClass (errorClass) => error instanceof errorClass
); );
} }
@ -43,23 +40,14 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
cb?.(); cb?.();
} }
function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) { function getToastProps(errorLogObj: ErrorLog, cb?: Function) {
const props: ToastOptions = { const props: ToastOptions = {
message: errorLogObj.name ?? t`Error`, message: errorLogObj.name ?? t`Error`,
type: 'error', type: 'error',
actionText: t`Report Error`,
action: () => reportIssue(errorLogObj),
}; };
// @ts-ignore
if (!canLog) {
Object.assign(props, {
actionText: t`Report Error`,
action: () => {
reportIssue(errorLogObj);
reportError(errorLogObj, cb);
},
});
}
return props; return props;
} }
@ -76,13 +64,12 @@ export function getErrorLogObject(
return errorLogObj; return errorLogObj;
} }
export function handleError( export async function handleError(
shouldLog: boolean, shouldLog: boolean,
error: Error, error: Error,
more?: Record<string, unknown>, more?: Record<string, unknown>,
cb?: Function cb?: Function
) { ) {
fyo.telemetry.error(error.name);
if (shouldLog) { if (shouldLog) {
console.error(error); console.error(error);
} }
@ -93,17 +80,14 @@ export function handleError(
const errorLogObj = getErrorLogObject(error, more ?? {}); const errorLogObj = getErrorLogObject(error, more ?? {});
const canLog = getCanLog(); await reportError(errorLogObj, cb);
if (canLog) { const toastProps = getToastProps(errorLogObj, cb);
reportError(errorLogObj, cb); await showToast(toastProps);
} else {
showToast(getToastProps(errorLogObj, canLog, cb));
}
} }
export async function handleErrorWithDialog(error: Error, doc?: Doc) { export async function handleErrorWithDialog(error: Error, doc?: Doc) {
const errorMessage = getErrorMessage(error, doc); const errorMessage = getErrorMessage(error, doc);
handleError(false, error, { errorMessage, doc }); await handleError(false, error, { errorMessage, doc });
const name = error.name ?? t`Error`; const name = error.name ?? t`Error`;
await showMessageDialog({ message: name, detail: errorMessage }); await showMessageDialog({ message: name, detail: errorMessage });
@ -125,7 +109,7 @@ export function getErrorHandled(func: Function) {
try { try {
return await func(...args); return await func(...args);
} catch (error) { } catch (error) {
handleError(false, error as Error, { await handleError(false, error as Error, {
functionName: func.name, functionName: func.name,
functionArgs: args, functionArgs: args,
}); });
@ -143,9 +127,9 @@ export function getErrorHandledSync(func: Function) {
handleError(false, error as Error, { handleError(false, error as Error, {
functionName: func.name, functionName: func.name,
functionArgs: args, functionArgs: args,
}).then(() => {
throw error;
}); });
throw error;
} }
}; };
} }

View File

@ -1,7 +1,7 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { getRegionalModels, models } from 'models'; import { getRegionalModels, models } from 'models';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { getValueMapFromList } from 'utils'; import { getRandomString, getValueMapFromList } from 'utils';
export const fyo = new Fyo({ isTest: false, isElectron: true }); export const fyo = new Fyo({ isTest: false, isElectron: true });
@ -32,6 +32,7 @@ export async function initializeInstance(
await setSingles(fyo); await setSingles(fyo);
await setCreds(fyo); await setCreds(fyo);
await setVersion(fyo); await setVersion(fyo);
await setInstanceId(fyo);
await setCurrencySymbols(fyo); await setCurrencySymbols(fyo);
} }
@ -64,6 +65,13 @@ async function setVersion(fyo: Fyo) {
} }
} }
async function setInstanceId(fyo: Fyo) {
const systemSettings = await fyo.doc.getSingle(ModelNameEnum.SystemSettings);
if (!systemSettings.instanceId) {
await systemSettings.setAndSync('instanceId', getRandomString());
}
}
async function setCurrencySymbols(fyo: Fyo) { async function setCurrencySymbols(fyo: Fyo) {
const currencies = (await fyo.db.getAll(ModelNameEnum.Currency, { const currencies = (await fyo.db.getAll(ModelNameEnum.Currency, {
fields: ['name', 'symbol'], fields: ['name', 'symbol'],

View File

@ -119,21 +119,38 @@
<!-- Language Selector --> <!-- Language Selector -->
<div <div
class="w-full flex justify-between items-center absolute px-6 py-6" class="
w-full
flex
justify-between
items-center
absolute
px-6
py-6
text-gray-900
"
style="top: 100%; transform: translateY(-100%)" style="top: 100%; transform: translateY(-100%)"
> >
<Button
class="text-sm w-40"
@click="createDemo"
:disabled="creatingDemo"
>{{ creatingDemo ? t`Please Wait` : t`Create Demo` }}</Button
>
<LanguageSelector <LanguageSelector
v-show="!creatingDemo" v-show="!creatingDemo"
class="w-40 bg-gray-100 rounded-md" class="text-sm w-40 bg-gray-100 rounded-md"
input-class="text-sm bg-transparent" input-class="py-1.5 bg-transparent"
/> />
<button
class="
text-sm
bg-gray-100
hover:bg-gray-200
rounded-md
px-4
py-1.5
w-40
"
@click="createDemo"
:disabled="creatingDemo"
>
{{ creatingDemo ? t`Please Wait` : t`Create Demo` }}
</button>
</div> </div>
</div> </div>
<Loading <Loading
@ -153,7 +170,6 @@ import { t } from 'fyo';
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { addNewFile } from 'fyo/telemetry/helpers'; import { addNewFile } from 'fyo/telemetry/helpers';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import Button from 'src/components/Button.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue'; import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import FeatherIcon from 'src/components/FeatherIcon.vue'; import FeatherIcon from 'src/components/FeatherIcon.vue';
import Loading from 'src/components/Loading.vue'; import Loading from 'src/components/Loading.vue';
@ -217,7 +233,7 @@ export default {
this.creatingDemo = true; this.creatingDemo = true;
const baseCount = fyo.store.isDevelopment ? 1000 : 150; const baseCount = fyo.store.isDevelopment ? 1000 : 150;
const companyName = await setupDummyInstance( const { companyName, instanceId } = await setupDummyInstance(
filePath, filePath,
fyo, fyo,
1, 1,
@ -230,9 +246,10 @@ export default {
addNewFile( addNewFile(
companyName, companyName,
fyo, filePath,
fyo.config.get(ConfigKeys.Files, []), instanceId,
filePath fyo.config.get(ConfigKeys.Files),
fyo
); );
fyo.purgeCache(); fyo.purgeCache();
@ -292,7 +309,6 @@ export default {
components: { components: {
LanguageSelector, LanguageSelector,
WindowControls, WindowControls,
Button,
Loading, Loading,
FeatherIcon, FeatherIcon,
}, },

View File

@ -8,26 +8,22 @@
:emit-change="true" :emit-change="true"
@change="forwardChangeEvent" @change="forwardChangeEvent"
/> />
<div class="flex flex-row justify-between items-center w-full"> <div
<div class="flex items-center"> class="flex flex-row justify-between items-center w-full text-gray-900"
<FormControl >
:df="df" <LanguageSelector
:value="telemetry" class="text-sm w-40 bg-gray-100 rounded-md"
@change="setValue" input-class="py-1.5 bg-transparent"
class="text-sm py-0 w-44" />
:label-right="false"
/>
<div class="border-r h-6 mx-2" />
<LanguageSelector class="text-sm w-44" input-class="py-2" />
</div>
<button <button
class=" class="
text-gray-900 text-sm text-sm
bg-gray-100 bg-gray-100
hover:bg-gray-200 hover:bg-gray-200
rounded-md rounded-md
px-4 px-4
py-1.5 py-1.5
w-40
" "
@click="checkForUpdates(true)" @click="checkForUpdates(true)"
> >
@ -39,10 +35,7 @@
<script> <script>
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { getTelemetryOptions } from 'fyo/telemetry/helpers';
import { NounEnum, TelemetrySetting, Verb } from 'fyo/telemetry/types';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import FormControl from 'src/components/Controls/FormControl.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue'; import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import TwoColumnForm from 'src/components/TwoColumnForm'; import TwoColumnForm from 'src/components/TwoColumnForm';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
@ -52,7 +45,6 @@ import { getCountryInfo } from 'utils/misc';
export default { export default {
name: 'TabSystem', name: 'TabSystem',
components: { components: {
FormControl,
TwoColumnForm, TwoColumnForm,
LanguageSelector, LanguageSelector,
}, },
@ -67,22 +59,9 @@ export default {
this.doc = fyo.singles.SystemSettings; this.doc = fyo.singles.SystemSettings;
this.companyName = fyo.singles.AccountingSettings.companyName; this.companyName = fyo.singles.AccountingSettings.companyName;
this.telemetry = fyo.config.get(ConfigKeys.Telemetry); this.telemetry = fyo.config.get(ConfigKeys.Telemetry);
window.gci = getCountryInfo window.gci = getCountryInfo;
}, },
computed: { computed: {
df() {
const telemetryOptions = getTelemetryOptions();
return {
fieldname: 'anonymizedTelemetry',
label: this.t`Anonymized Telemetry`,
fieldtype: 'Select',
options: Object.keys(telemetryOptions),
map: telemetryOptions,
default: 'allow',
description: this
.t`Send anonymized usage data and error reports to help improve the product.`,
};
},
fields() { fields() {
return fyo.schemaMap.SystemSettings.quickEditFields.map((f) => return fyo.schemaMap.SystemSettings.quickEditFields.map((f) =>
fyo.getField(ModelNameEnum.SystemSettings, f) fyo.getField(ModelNameEnum.SystemSettings, f)
@ -91,16 +70,6 @@ export default {
}, },
methods: { methods: {
checkForUpdates, checkForUpdates,
setValue(value) {
this.telemetry = value;
if (value === TelemetrySetting.dontLogAnything) {
fyo.telemetry.finalLogAndStop();
} else {
fyo.telemetry.log(Verb.Started, NounEnum.Telemetry);
}
fyo.config.set(ConfigKeys.Telemetry, value);
},
forwardChangeEvent(...args) { forwardChangeEvent(...args) {
this.$emit('change', ...args); this.$emit('change', ...args);
}, },

View File

@ -81,7 +81,6 @@ function setErrorHandlers(app: VueApp) {
const { fullPath, params } = vm.$route; const { fullPath, params } = vm.$route;
more.fullPath = fullPath; more.fullPath = fullPath;
more.params = stringifyCircular(params ?? {}); more.params = stringifyCircular(params ?? {});
more.data = stringifyCircular(vm.$data ?? {}, true, true);
more.props = stringifyCircular(vm.$props ?? {}, true, true); more.props = stringifyCircular(vm.$props ?? {}, true, true);
} }

View File

@ -1,7 +1,6 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { handleError } from 'src/errorHandling'; import { handleError } from 'src/errorHandling';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { startTelemetry } from 'src/utils/misc';
import { showToast } from 'src/utils/ui'; import { showToast } from 'src/utils/ui';
import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages'; import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
@ -57,7 +56,7 @@ export default function registerIpcRendererListeners() {
document.addEventListener('visibilitychange', function () { document.addEventListener('visibilitychange', function () {
const { visibilityState } = document; const { visibilityState } = document;
if (visibilityState === 'visible' && !fyo.telemetry.started) { if (visibilityState === 'visible' && !fyo.telemetry.started) {
startTelemetry(); fyo.telemetry.start();
} }
if (visibilityState !== 'hidden') { if (visibilityState !== 'hidden') {

View File

@ -1,4 +1,3 @@
import { NounEnum, Verb } from 'fyo/telemetry/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue'; import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue'; import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import DataImport from 'src/pages/DataImport.vue'; import DataImport from 'src/pages/DataImport.vue';
@ -11,7 +10,6 @@ import QuickEditForm from 'src/pages/QuickEditForm.vue';
import Report from 'src/pages/Report.vue'; import Report from 'src/pages/Report.vue';
import Settings from 'src/pages/Settings/Settings.vue'; import Settings from 'src/pages/Settings/Settings.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { fyo } from './initFyo';
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ {
@ -120,26 +118,4 @@ const routes: RouteRecordRaw[] = [
const router = createRouter({ routes, history: createWebHistory() }); const router = createRouter({ routes, history: createWebHistory() });
function removeDetails(path: string) {
if (!path) {
return path;
}
const match = path.match(/edit=1/);
if (!match) {
return path;
}
return path.slice(0, match.index! + 4);
}
router.afterEach((to, from) => {
const more = {
from: removeDetails(from.fullPath),
to: removeDetails(to.fullPath),
};
fyo.telemetry.log(Verb.Navigated, NounEnum.Route, more);
});
export default router; export default router;

View File

@ -2,15 +2,16 @@ import { Fyo } from 'fyo';
import { ConfigFile, DocValueMap } from 'fyo/core/types'; import { ConfigFile, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { createNumberSeries } from 'fyo/model/naming'; import { createNumberSeries } from 'fyo/model/naming';
import { getId } from 'fyo/telemetry/helpers';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_LOCALE, DEFAULT_LOCALE,
DEFAULT_SERIES_START, DEFAULT_SERIES_START,
} from 'fyo/utils/consts'; } from 'fyo/utils/consts';
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings'; import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
import { ModelNameEnum } from 'models/types';
import { initializeInstance } from 'src/initFyo'; import { initializeInstance } from 'src/initFyo';
import { createRegionalRecords } from 'src/regional'; import { createRegionalRecords } from 'src/regional';
import { getRandomString } from 'utils';
import { getCountryCodeFromCountry, getCountryInfo } from 'utils/misc'; import { getCountryCodeFromCountry, getCountryInfo } from 'utils/misc';
import { CountryInfo } from 'utils/types'; import { CountryInfo } from 'utils/types';
import { CreateCOA } from './createCOA'; import { CreateCOA } from './createCOA';
@ -93,10 +94,12 @@ async function updateSystemSettings(
const locale = countryOptions.locale ?? DEFAULT_LOCALE; const locale = countryOptions.locale ?? DEFAULT_LOCALE;
const countryCode = getCountryCodeFromCountry(country); const countryCode = getCountryCodeFromCountry(country);
const systemSettings = await fyo.doc.getSingle('SystemSettings'); const systemSettings = await fyo.doc.getSingle('SystemSettings');
const instanceId = getRandomString();
systemSettings.setAndSync({ systemSettings.setAndSync({
locale, locale,
currency, currency,
instanceId,
countryCode, countryCode,
}); });
} }
@ -156,18 +159,22 @@ async function createAccountRecords(
} }
async function completeSetup(companyName: string, fyo: Fyo) { async function completeSetup(companyName: string, fyo: Fyo) {
updateInitializationConfig(companyName, fyo); await updateInitializationConfig(companyName, fyo);
await fyo.singles.AccountingSettings!.setAndSync('setupComplete', true); await fyo.singles.AccountingSettings!.setAndSync('setupComplete', true);
} }
function updateInitializationConfig(companyName: string, fyo: Fyo) { async function updateInitializationConfig(companyName: string, fyo: Fyo) {
const instanceId = (await fyo.getValue(
ModelNameEnum.SystemSettings,
'instanceId'
)) as string;
const dbPath = fyo.db.dbPath; const dbPath = fyo.db.dbPath;
const files = fyo.config.get('files', []) as ConfigFile[]; const files = fyo.config.get('files', []) as ConfigFile[];
files.forEach((file) => { files.forEach((file) => {
if (file.dbPath === dbPath) { if (file.dbPath === dbPath) {
file.companyName = companyName; file.companyName = companyName;
file.id = getId(); file.id = instanceId;
} }
}); });

View File

@ -71,6 +71,8 @@ export async function incrementOpenCount(dbPath: string) {
ModelNameEnum.AccountingSettings, ModelNameEnum.AccountingSettings,
'companyName' 'companyName'
)) as string; )) as string;
let openCount = 0;
const files = fyo.config.get(ConfigKeys.Files) as ConfigFile[]; const files = fyo.config.get(ConfigKeys.Files) as ConfigFile[];
for (const file of files) { for (const file of files) {
if (file.companyName !== companyName || file.dbPath !== dbPath) { if (file.companyName !== companyName || file.dbPath !== dbPath) {
@ -79,20 +81,10 @@ export async function incrementOpenCount(dbPath: string) {
file.openCount ??= 0; file.openCount ??= 0;
file.openCount += 1; file.openCount += 1;
openCount = file.openCount;
break; break;
} }
fyo.config.set(ConfigKeys.Files, files); fyo.config.set(ConfigKeys.Files, files);
} return openCount;
export async function startTelemetry() {
fyo.telemetry.interestingDocs = [
ModelNameEnum.Payment,
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
ModelNameEnum.Party,
ModelNameEnum.Item,
];
await fyo.telemetry.start();
} }