mirror of
https://github.com/frappe/books.git
synced 2024-12-22 19:09:01 +00:00
incr: simplify telemetry
- it's not opt-in anymore cause my job deps on it
This commit is contained in:
parent
deefcd08f9
commit
2c596b11c8
@ -48,7 +48,12 @@ export async function setupDummyInstance(
|
||||
notifier?.(fyo.t`Creating Items and Parties`, -1);
|
||||
await generateStaticEntries(fyo);
|
||||
await generateDynamicEntries(fyo, years, baseCount, notifier);
|
||||
return options.companyName;
|
||||
|
||||
const instanceId = (await fyo.getValue(
|
||||
ModelNameEnum.SystemSettings,
|
||||
'instanceId'
|
||||
)) as string;
|
||||
return { companyName: options.companyName, instanceId };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,7 +34,6 @@ export enum ConfigKeys {
|
||||
LastSelectedFilePath = 'lastSelectedFilePath',
|
||||
Language = 'language',
|
||||
DeviceId = 'deviceId',
|
||||
Telemetry = 'telemetry',
|
||||
}
|
||||
|
||||
export interface ConfigFile {
|
||||
|
@ -678,6 +678,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
await this.setAndSync('submitted', true);
|
||||
await this.trigger('afterSubmit');
|
||||
|
||||
this.fyo.telemetry.log(Verb.Submitted, this.schemaName);
|
||||
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.trigger('afterCancel');
|
||||
|
||||
this.fyo.telemetry.log(Verb.Cancelled, this.schemaName);
|
||||
this.fyo.doc.observer.trigger(`cancel:${this.schemaName}`, this.name);
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,9 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||
import { DEFAULT_COUNTRY_CODE } from 'fyo/utils/consts';
|
||||
import { t } from 'fyo/utils/translation';
|
||||
import { Count, TelemetrySetting, 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;
|
||||
}
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { getRandomString } from 'utils';
|
||||
import { UniqueId } from './types';
|
||||
|
||||
export function getCountry(fyo: Fyo): string {
|
||||
return (
|
||||
@ -24,27 +15,10 @@ export function getLanguage(fyo: Fyo): 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 {
|
||||
let deviceId = fyo.config.get(ConfigKeys.DeviceId) as string | undefined;
|
||||
if (deviceId === undefined) {
|
||||
deviceId = getId();
|
||||
deviceId = getRandomString();
|
||||
fyo.config.set(ConfigKeys.DeviceId, deviceId);
|
||||
}
|
||||
|
||||
@ -53,69 +27,69 @@ export function getDeviceId(fyo: Fyo): UniqueId {
|
||||
|
||||
export function getInstanceId(fyo: Fyo): UniqueId {
|
||||
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;
|
||||
if (companyName === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const file = files.find((f) => f.companyName === companyName);
|
||||
let file = files.find((f) => f.id === instanceId);
|
||||
|
||||
if (file === undefined) {
|
||||
return addNewFile(companyName, fyo, files);
|
||||
file = addNewConfigFile(companyName, dbPath, instanceId, files, fyo);
|
||||
}
|
||||
|
||||
if (file.id === undefined) {
|
||||
return setInstanceId(companyName, files, fyo);
|
||||
if (!file.id) {
|
||||
setIdOnConfigFile(instanceId, companyName, dbPath, files, fyo);
|
||||
}
|
||||
|
||||
return file.id;
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
export function addNewFile(
|
||||
export function addNewConfigFile(
|
||||
companyName: string,
|
||||
fyo: Fyo,
|
||||
files?: ConfigFile[],
|
||||
dbPath?: string
|
||||
): UniqueId {
|
||||
files ??= fyo.config.get(ConfigKeys.Files, []) as ConfigFile[];
|
||||
dbPath ??= fyo.config.get(ConfigKeys.LastSelectedFilePath, '') as string;
|
||||
|
||||
dbPath: string,
|
||||
instanceId: string,
|
||||
files: ConfigFile[],
|
||||
fyo: Fyo
|
||||
): ConfigFile {
|
||||
const newFile: ConfigFile = {
|
||||
companyName,
|
||||
dbPath,
|
||||
id: getId(),
|
||||
id: instanceId,
|
||||
openCount: 0,
|
||||
};
|
||||
|
||||
files.push(newFile);
|
||||
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,
|
||||
dbPath: string,
|
||||
files: ConfigFile[],
|
||||
fyo: Fyo
|
||||
): UniqueId {
|
||||
let id = '';
|
||||
) {
|
||||
for (const file of files) {
|
||||
if (file.id) {
|
||||
if (file.companyName !== companyName || file.dbPath !== dbPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
file.id = getId();
|
||||
if (file.companyName === companyName) {
|
||||
id = file.id;
|
||||
}
|
||||
file.id = instanceId;
|
||||
}
|
||||
|
||||
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`,
|
||||
});
|
||||
|
@ -1,53 +1,38 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
getCountry,
|
||||
getCounts,
|
||||
getDeviceId,
|
||||
getInstanceId,
|
||||
getLanguage,
|
||||
getVersion,
|
||||
} from './helpers';
|
||||
import {
|
||||
Noun,
|
||||
NounEnum,
|
||||
Platform,
|
||||
Telemetry,
|
||||
TelemetrySetting,
|
||||
Verb,
|
||||
} from './types';
|
||||
import { Noun, Platform, Telemetry, Verb } from './types';
|
||||
|
||||
/**
|
||||
* # Telemetry
|
||||
* Used to check if people are using Books or not. All logging
|
||||
* happens using navigator.sendBeacon
|
||||
*
|
||||
* ## `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:
|
||||
* 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
|
||||
* then comes back later.
|
||||
* 3. When `log` is called if not initialized.
|
||||
* 3. When `log` is called, but telemetry wasn't 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.
|
||||
* Used to log activity.
|
||||
*
|
||||
* ## `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.
|
||||
*/
|
||||
|
||||
export class TelemetryManager {
|
||||
@ -55,7 +40,6 @@ export class TelemetryManager {
|
||||
#token: string = '';
|
||||
#started = false;
|
||||
#telemetryObject: Partial<Telemetry> = {};
|
||||
#interestingDocs: string[] = [];
|
||||
fyo: Fyo;
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
@ -66,10 +50,6 @@ export class TelemetryManager {
|
||||
this.#telemetryObject.platform ||= value;
|
||||
}
|
||||
|
||||
set interestingDocs(schemaNames: string[]) {
|
||||
this.#interestingDocs = schemaNames;
|
||||
}
|
||||
|
||||
get hasCreds() {
|
||||
return !!this.#url && !!this.#token;
|
||||
}
|
||||
@ -82,89 +62,57 @@ export class TelemetryManager {
|
||||
return cloneDeep(this.#telemetryObject);
|
||||
}
|
||||
|
||||
async start() {
|
||||
async start(openCount?: number) {
|
||||
this.#telemetryObject.country ||= getCountry(this.fyo);
|
||||
this.#telemetryObject.language ??= getLanguage(this.fyo);
|
||||
this.#telemetryObject.deviceId ||= getDeviceId(this.fyo);
|
||||
this.#telemetryObject.instanceId ||= getInstanceId(this.fyo);
|
||||
this.#telemetryObject.openTime ||= new Date().valueOf();
|
||||
this.#telemetryObject.timeline ??= [];
|
||||
this.#telemetryObject.errors ??= {};
|
||||
this.#telemetryObject.counts ??= {};
|
||||
this.#telemetryObject.device ||= getDeviceId(this.fyo);
|
||||
this.#telemetryObject.instance ||= getInstanceId(this.fyo);
|
||||
this.#telemetryObject.version ||= await getVersion(this.fyo);
|
||||
|
||||
this.#started = true;
|
||||
await this.#setCreds();
|
||||
|
||||
await this.#postStart();
|
||||
}
|
||||
|
||||
async log(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
|
||||
if (!this.#started) {
|
||||
await this.start();
|
||||
if (typeof openCount === 'number') {
|
||||
this.#telemetryObject.openCount = openCount;
|
||||
this.log(Verb.Started, 'telemetry');
|
||||
} else {
|
||||
this.log(Verb.Resumed, 'telemetry');
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log(Verb.Stopped, 'telemetry');
|
||||
this.#started = false;
|
||||
this.#clear();
|
||||
}
|
||||
|
||||
this.#telemetryObject.version = this.fyo.store.appVersion ?? '';
|
||||
this.#telemetryObject.closeTime = new Date().valueOf();
|
||||
log(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
|
||||
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({
|
||||
token: this.#token,
|
||||
telemetryData: this.#telemetryObject,
|
||||
telemetryData,
|
||||
});
|
||||
|
||||
this.#clear();
|
||||
|
||||
if (
|
||||
this.fyo.config.get(ConfigKeys.Telemetry) ===
|
||||
TelemetrySetting.dontLogAnything
|
||||
) {
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
if (this.hasCreds) {
|
||||
return;
|
||||
@ -175,21 +123,31 @@ export class TelemetryManager {
|
||||
this.#token = token;
|
||||
}
|
||||
|
||||
#getCanLog(): boolean {
|
||||
const telemetrySetting = this.fyo.config.get(
|
||||
ConfigKeys.Telemetry
|
||||
) as string;
|
||||
return telemetrySetting === TelemetrySetting.allow;
|
||||
#getTelemtryData(
|
||||
verb: Verb,
|
||||
noun: Noun,
|
||||
more?: Record<string, unknown>
|
||||
): 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() {
|
||||
// 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.language;
|
||||
delete this.#telemetryObject.device;
|
||||
delete this.#telemetryObject.instance;
|
||||
delete this.#telemetryObject.version;
|
||||
delete this.#telemetryObject.openCount;
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +1,33 @@
|
||||
export type AppVersion = 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 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 {
|
||||
Created = 'created',
|
||||
Deleted = 'deleted',
|
||||
Navigated = 'navigated',
|
||||
Submitted = 'submitted',
|
||||
Cancelled = 'cancelled',
|
||||
Imported = 'imported',
|
||||
Exported = 'exported',
|
||||
Stopped = 'stopped',
|
||||
Started = 'stopped',
|
||||
Started = 'started',
|
||||
Resumed = 'resumed',
|
||||
}
|
||||
|
||||
export enum NounEnum {
|
||||
Route = 'route',
|
||||
Telemetry = 'telemetry',
|
||||
}
|
||||
export type Noun = string;
|
||||
|
||||
export type Noun = string | NounEnum;
|
||||
|
||||
export enum TelemetrySetting {
|
||||
allow = 'allow',
|
||||
dontLogUsage = 'dontLogUsage',
|
||||
dontLogAnything = 'dontLogAnything',
|
||||
export interface Telemetry {
|
||||
device: UniqueId;
|
||||
instance: UniqueId;
|
||||
platform?: Platform;
|
||||
country: string;
|
||||
language: string;
|
||||
version: AppVersion;
|
||||
timestamp: Timestamp;
|
||||
openCount: number;
|
||||
verb: Verb;
|
||||
noun: Noun;
|
||||
more?: Record<string, unknown>
|
||||
}
|
||||
|
@ -95,6 +95,12 @@
|
||||
"label": "Version",
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "instanceId",
|
||||
"label": "Instance Id",
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
|
15
src/App.vue
15
src/App.vue
@ -29,20 +29,12 @@
|
||||
>
|
||||
<div id="toast-target" />
|
||||
</div>
|
||||
|
||||
<!-- Prompt to Set Telemetry -->
|
||||
<TelemetryModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import {
|
||||
getSetupComplete,
|
||||
incrementOpenCount,
|
||||
startTelemetry
|
||||
} from 'src/utils/misc';
|
||||
import TelemetryModal from './components/once/TelemetryModal.vue';
|
||||
import { getSetupComplete, incrementOpenCount } from 'src/utils/misc';
|
||||
import WindowsTitleBar from './components/WindowsTitleBar.vue';
|
||||
import { fyo, initializeInstance } from './initFyo';
|
||||
import DatabaseSelector from './pages/DatabaseSelector.vue';
|
||||
@ -65,7 +57,6 @@ export default {
|
||||
SetupWizard,
|
||||
DatabaseSelector,
|
||||
WindowsTitleBar,
|
||||
TelemetryModal,
|
||||
},
|
||||
async mounted() {
|
||||
fyo.telemetry.platform = this.platform;
|
||||
@ -86,8 +77,8 @@ export default {
|
||||
async setDesk(filePath) {
|
||||
this.activeScreen = 'Desk';
|
||||
await this.setDeskRoute();
|
||||
await incrementOpenCount(filePath);
|
||||
await startTelemetry();
|
||||
const openCount = await incrementOpenCount(filePath);
|
||||
await fyo.telemetry.start(openCount);
|
||||
await checkForUpdates(false);
|
||||
},
|
||||
async fileSelected(filePath, isNew) {
|
||||
|
@ -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>
|
@ -1,9 +1,11 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { t } from 'fyo';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { TelemetrySetting } from 'fyo/telemetry/types';
|
||||
import { MandatoryError, ValidationError } from 'fyo/utils/errors';
|
||||
import {
|
||||
MandatoryError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
} from 'fyo/utils/errors';
|
||||
import { ErrorLog } from 'fyo/utils/types';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import { fyo } from './initFyo';
|
||||
@ -11,13 +13,8 @@ import { getErrorMessage } from './utils';
|
||||
import { ToastOptions } from './utils/types';
|
||||
import { showMessageDialog, showToast } from './utils/ui';
|
||||
|
||||
function getCanLog(): boolean {
|
||||
const telemetrySetting = fyo.config.get(ConfigKeys.Telemetry);
|
||||
return telemetrySetting !== TelemetrySetting.dontLogAnything;
|
||||
}
|
||||
|
||||
function shouldNotStore(error: Error) {
|
||||
return [MandatoryError, ValidationError].some(
|
||||
return [MandatoryError, ValidationError, NotFoundError].some(
|
||||
(errorClass) => error instanceof errorClass
|
||||
);
|
||||
}
|
||||
@ -43,23 +40,14 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
|
||||
cb?.();
|
||||
}
|
||||
|
||||
function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) {
|
||||
function getToastProps(errorLogObj: ErrorLog, cb?: Function) {
|
||||
const props: ToastOptions = {
|
||||
message: errorLogObj.name ?? t`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;
|
||||
}
|
||||
|
||||
@ -76,13 +64,12 @@ export function getErrorLogObject(
|
||||
return errorLogObj;
|
||||
}
|
||||
|
||||
export function handleError(
|
||||
export async function handleError(
|
||||
shouldLog: boolean,
|
||||
error: Error,
|
||||
more?: Record<string, unknown>,
|
||||
cb?: Function
|
||||
) {
|
||||
fyo.telemetry.error(error.name);
|
||||
if (shouldLog) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -93,17 +80,14 @@ export function handleError(
|
||||
|
||||
const errorLogObj = getErrorLogObject(error, more ?? {});
|
||||
|
||||
const canLog = getCanLog();
|
||||
if (canLog) {
|
||||
reportError(errorLogObj, cb);
|
||||
} else {
|
||||
showToast(getToastProps(errorLogObj, canLog, cb));
|
||||
}
|
||||
await reportError(errorLogObj, cb);
|
||||
const toastProps = getToastProps(errorLogObj, cb);
|
||||
await showToast(toastProps);
|
||||
}
|
||||
|
||||
export async function handleErrorWithDialog(error: Error, doc?: Doc) {
|
||||
const errorMessage = getErrorMessage(error, doc);
|
||||
handleError(false, error, { errorMessage, doc });
|
||||
await handleError(false, error, { errorMessage, doc });
|
||||
|
||||
const name = error.name ?? t`Error`;
|
||||
await showMessageDialog({ message: name, detail: errorMessage });
|
||||
@ -125,7 +109,7 @@ export function getErrorHandled(func: Function) {
|
||||
try {
|
||||
return await func(...args);
|
||||
} catch (error) {
|
||||
handleError(false, error as Error, {
|
||||
await handleError(false, error as Error, {
|
||||
functionName: func.name,
|
||||
functionArgs: args,
|
||||
});
|
||||
@ -143,9 +127,9 @@ export function getErrorHandledSync(func: Function) {
|
||||
handleError(false, error as Error, {
|
||||
functionName: func.name,
|
||||
functionArgs: args,
|
||||
}).then(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { getRegionalModels, models } from 'models';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { getValueMapFromList } from 'utils';
|
||||
import { getRandomString, getValueMapFromList } from 'utils';
|
||||
|
||||
export const fyo = new Fyo({ isTest: false, isElectron: true });
|
||||
|
||||
@ -32,6 +32,7 @@ export async function initializeInstance(
|
||||
await setSingles(fyo);
|
||||
await setCreds(fyo);
|
||||
await setVersion(fyo);
|
||||
await setInstanceId(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) {
|
||||
const currencies = (await fyo.db.getAll(ModelNameEnum.Currency, {
|
||||
fields: ['name', 'symbol'],
|
||||
|
@ -119,21 +119,38 @@
|
||||
|
||||
<!-- Language Selector -->
|
||||
<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%)"
|
||||
>
|
||||
<Button
|
||||
class="text-sm w-40"
|
||||
@click="createDemo"
|
||||
:disabled="creatingDemo"
|
||||
>{{ creatingDemo ? t`Please Wait` : t`Create Demo` }}</Button
|
||||
>
|
||||
|
||||
<LanguageSelector
|
||||
v-show="!creatingDemo"
|
||||
class="w-40 bg-gray-100 rounded-md"
|
||||
input-class="text-sm bg-transparent"
|
||||
class="text-sm w-40 bg-gray-100 rounded-md"
|
||||
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>
|
||||
<Loading
|
||||
@ -153,7 +170,6 @@ import { t } from 'fyo';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { addNewFile } from 'fyo/telemetry/helpers';
|
||||
import { DateTime } from 'luxon';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
|
||||
import FeatherIcon from 'src/components/FeatherIcon.vue';
|
||||
import Loading from 'src/components/Loading.vue';
|
||||
@ -217,7 +233,7 @@ export default {
|
||||
this.creatingDemo = true;
|
||||
const baseCount = fyo.store.isDevelopment ? 1000 : 150;
|
||||
|
||||
const companyName = await setupDummyInstance(
|
||||
const { companyName, instanceId } = await setupDummyInstance(
|
||||
filePath,
|
||||
fyo,
|
||||
1,
|
||||
@ -230,9 +246,10 @@ export default {
|
||||
|
||||
addNewFile(
|
||||
companyName,
|
||||
fyo,
|
||||
fyo.config.get(ConfigKeys.Files, []),
|
||||
filePath
|
||||
filePath,
|
||||
instanceId,
|
||||
fyo.config.get(ConfigKeys.Files),
|
||||
fyo
|
||||
);
|
||||
|
||||
fyo.purgeCache();
|
||||
@ -292,7 +309,6 @@ export default {
|
||||
components: {
|
||||
LanguageSelector,
|
||||
WindowControls,
|
||||
Button,
|
||||
Loading,
|
||||
FeatherIcon,
|
||||
},
|
||||
|
@ -8,26 +8,22 @@
|
||||
:emit-change="true"
|
||||
@change="forwardChangeEvent"
|
||||
/>
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<div class="flex items-center">
|
||||
<FormControl
|
||||
:df="df"
|
||||
:value="telemetry"
|
||||
@change="setValue"
|
||||
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>
|
||||
<div
|
||||
class="flex flex-row justify-between items-center w-full text-gray-900"
|
||||
>
|
||||
<LanguageSelector
|
||||
class="text-sm w-40 bg-gray-100 rounded-md"
|
||||
input-class="py-1.5 bg-transparent"
|
||||
/>
|
||||
<button
|
||||
class="
|
||||
text-gray-900 text-sm
|
||||
text-sm
|
||||
bg-gray-100
|
||||
hover:bg-gray-200
|
||||
rounded-md
|
||||
px-4
|
||||
py-1.5
|
||||
w-40
|
||||
"
|
||||
@click="checkForUpdates(true)"
|
||||
>
|
||||
@ -39,10 +35,7 @@
|
||||
|
||||
<script>
|
||||
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 FormControl from 'src/components/Controls/FormControl.vue';
|
||||
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
|
||||
import TwoColumnForm from 'src/components/TwoColumnForm';
|
||||
import { fyo } from 'src/initFyo';
|
||||
@ -52,7 +45,6 @@ import { getCountryInfo } from 'utils/misc';
|
||||
export default {
|
||||
name: 'TabSystem',
|
||||
components: {
|
||||
FormControl,
|
||||
TwoColumnForm,
|
||||
LanguageSelector,
|
||||
},
|
||||
@ -67,22 +59,9 @@ export default {
|
||||
this.doc = fyo.singles.SystemSettings;
|
||||
this.companyName = fyo.singles.AccountingSettings.companyName;
|
||||
this.telemetry = fyo.config.get(ConfigKeys.Telemetry);
|
||||
window.gci = getCountryInfo
|
||||
window.gci = getCountryInfo;
|
||||
},
|
||||
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() {
|
||||
return fyo.schemaMap.SystemSettings.quickEditFields.map((f) =>
|
||||
fyo.getField(ModelNameEnum.SystemSettings, f)
|
||||
@ -91,16 +70,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
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) {
|
||||
this.$emit('change', ...args);
|
||||
},
|
||||
|
@ -81,7 +81,6 @@ function setErrorHandlers(app: VueApp) {
|
||||
const { fullPath, params } = vm.$route;
|
||||
more.fullPath = fullPath;
|
||||
more.params = stringifyCircular(params ?? {});
|
||||
more.data = stringifyCircular(vm.$data ?? {}, true, true);
|
||||
more.props = stringifyCircular(vm.$props ?? {}, true, true);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { handleError } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { startTelemetry } from 'src/utils/misc';
|
||||
import { showToast } from 'src/utils/ui';
|
||||
import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
|
||||
|
||||
@ -57,7 +56,7 @@ export default function registerIpcRendererListeners() {
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
const { visibilityState } = document;
|
||||
if (visibilityState === 'visible' && !fyo.telemetry.started) {
|
||||
startTelemetry();
|
||||
fyo.telemetry.start();
|
||||
}
|
||||
|
||||
if (visibilityState !== 'hidden') {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { NounEnum, Verb } from 'fyo/telemetry/types';
|
||||
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
|
||||
import Dashboard from 'src/pages/Dashboard/Dashboard.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 Settings from 'src/pages/Settings/Settings.vue';
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||
import { fyo } from './initFyo';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@ -120,26 +118,4 @@ const routes: RouteRecordRaw[] = [
|
||||
|
||||
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;
|
||||
|
@ -2,15 +2,16 @@ import { Fyo } from 'fyo';
|
||||
import { ConfigFile, DocValueMap } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { createNumberSeries } from 'fyo/model/naming';
|
||||
import { getId } from 'fyo/telemetry/helpers';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_LOCALE,
|
||||
DEFAULT_SERIES_START,
|
||||
} from 'fyo/utils/consts';
|
||||
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { initializeInstance } from 'src/initFyo';
|
||||
import { createRegionalRecords } from 'src/regional';
|
||||
import { getRandomString } from 'utils';
|
||||
import { getCountryCodeFromCountry, getCountryInfo } from 'utils/misc';
|
||||
import { CountryInfo } from 'utils/types';
|
||||
import { CreateCOA } from './createCOA';
|
||||
@ -93,10 +94,12 @@ async function updateSystemSettings(
|
||||
const locale = countryOptions.locale ?? DEFAULT_LOCALE;
|
||||
const countryCode = getCountryCodeFromCountry(country);
|
||||
const systemSettings = await fyo.doc.getSingle('SystemSettings');
|
||||
const instanceId = getRandomString();
|
||||
|
||||
systemSettings.setAndSync({
|
||||
locale,
|
||||
currency,
|
||||
instanceId,
|
||||
countryCode,
|
||||
});
|
||||
}
|
||||
@ -156,18 +159,22 @@ async function createAccountRecords(
|
||||
}
|
||||
|
||||
async function completeSetup(companyName: string, fyo: Fyo) {
|
||||
updateInitializationConfig(companyName, fyo);
|
||||
await updateInitializationConfig(companyName, fyo);
|
||||
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 files = fyo.config.get('files', []) as ConfigFile[];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.dbPath === dbPath) {
|
||||
file.companyName = companyName;
|
||||
file.id = getId();
|
||||
file.id = instanceId;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -71,6 +71,8 @@ export async function incrementOpenCount(dbPath: string) {
|
||||
ModelNameEnum.AccountingSettings,
|
||||
'companyName'
|
||||
)) as string;
|
||||
|
||||
let openCount = 0;
|
||||
const files = fyo.config.get(ConfigKeys.Files) as ConfigFile[];
|
||||
for (const file of files) {
|
||||
if (file.companyName !== companyName || file.dbPath !== dbPath) {
|
||||
@ -79,20 +81,10 @@ export async function incrementOpenCount(dbPath: string) {
|
||||
|
||||
file.openCount ??= 0;
|
||||
file.openCount += 1;
|
||||
openCount = file.openCount;
|
||||
break;
|
||||
}
|
||||
|
||||
fyo.config.set(ConfigKeys.Files, files);
|
||||
}
|
||||
|
||||
export async function startTelemetry() {
|
||||
fyo.telemetry.interestingDocs = [
|
||||
ModelNameEnum.Payment,
|
||||
ModelNameEnum.SalesInvoice,
|
||||
ModelNameEnum.PurchaseInvoice,
|
||||
ModelNameEnum.JournalEntry,
|
||||
ModelNameEnum.Party,
|
||||
ModelNameEnum.Item,
|
||||
];
|
||||
await fyo.telemetry.start();
|
||||
return openCount;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user