2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 03:19:01 +00:00

Merge pull request #364 from 18alantom/opt-in-telem

chore: add anon opt in telemetry
This commit is contained in:
Alan 2022-03-15 12:09:51 +05:30 committed by GitHub
commit ac28496619
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 745 additions and 88 deletions

View File

@ -25,10 +25,12 @@ jobs:
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: |
echo $ERR_LOG_KEY > err_log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt
echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build
env:
@ -70,10 +72,12 @@ jobs:
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: |
echo $ERR_LOG_KEY > err_log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt
echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build
env:
@ -112,10 +116,12 @@ jobs:
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: |
echo $ERR_LOG_KEY > err_log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt
echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build
env:

2
.gitignore vendored
View File

@ -24,4 +24,4 @@ yarn-error.log*
#Electron-builder output
/dist_electron
err_log_creds.txt
log_creds.txt

View File

@ -89,7 +89,7 @@ There are many ways you can contribute even if you don't code:
1. If you find any issues, no matter how small (even typos), you can [raise an issue](https://github.com/frappe/books/issues/new) to inform us.
2. You can help us with language support by [contributing translations](https://github.com/frappe/books/wiki/Contributing-Translations).
3. You report errors by setting **Hide & Auto Report Errors** in _Settings > System_.
3. If you're a user, you can switch on [anonymized telemetry](https://github.com/frappe/books/wiki/Anonymized-Opt-In-Telemetry).
4. If you're an ardent user you can tell us what you would like to see.
5. If you have accounting requirements, you can become an ardent user. 🙂
6. You can join our [telegram group](https://t.me/frappebooks) and share your thoughts.

View File

@ -3,8 +3,8 @@ appId: io.frappe.books
afterSign: build/notarize.js
extraResources: [
{
from: 'err_log_creds.txt',
to: '../creds/err_log_creds.txt',
from: 'log_creds.txt',
to: '../creds/log_creds.txt',
}
]
mac:

View File

@ -383,4 +383,5 @@ module.exports = {
},
t,
T,
store: {},
};

View File

@ -3,6 +3,8 @@ const Observable = require('frappe/utils/observable');
const naming = require('./naming');
const { isPesa } = require('../utils/index');
const { DEFAULT_INTERNAL_PRECISION } = require('../utils/consts');
const { Verb } = require('@/telemetry/types');
const { default: telemetry } = require('@/telemetry/telemetry');
module.exports = class BaseDocument extends Observable {
constructor(data) {
@ -577,6 +579,7 @@ module.exports = class BaseDocument extends Observable {
await this.trigger('afterInsert');
await this.trigger('afterSave');
telemetry.log(Verb.Created, this.doctype);
return this;
}
@ -620,6 +623,8 @@ module.exports = class BaseDocument extends Observable {
await this.trigger('beforeDelete');
await frappe.db.delete(this.doctype, this.name);
await this.trigger('afterDelete');
telemetry.log(Verb.Deleted, this.doctype);
}
async submitOrRevert(isSubmit) {

View File

@ -84,19 +84,11 @@ module.exports = {
default: 0,
description: t`Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.`,
},
{
fieldname: 'autoReportErrors',
label: t`Hide & Auto Report Errors`,
fieldtype: 'Check',
default: 0,
description: t`Prevent errors from showing and automatically report all errors.`,
},
],
quickEditFields: [
'locale',
'dateFormat',
'displayPrecision',
'hideGetStarted',
'autoReportErrors',
],
};

53
models/types.ts Normal file
View File

@ -0,0 +1,53 @@
export enum DoctypeName {
SetupWizard = 'SetupWizard',
Currency = 'Currency',
Color = 'Color',
Account = 'Account',
AccountingSettings = 'AccountingSettings',
CompanySettings = 'CompanySettings',
AccountingLedgerEntry = 'AccountingLedgerEntry',
Party = 'Party',
Customer = 'Customer',
Supplier = 'Supplier',
Payment = 'Payment',
PaymentFor = 'PaymentFor',
PaymentSettings = 'PaymentSettings',
Item = 'Item',
SalesInvoice = 'SalesInvoice',
SalesInvoiceItem = 'SalesInvoiceItem',
SalesInvoiceSettings = 'SalesInvoiceSettings',
PurchaseInvoice = 'PurchaseInvoice',
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
PurchaseInvoiceSettings = 'PurchaseInvoiceSettings',
Tax = 'Tax',
TaxDetail = 'TaxDetail',
TaxSummary = 'TaxSummary',
GSTR3B = 'GSTR3B',
Address = 'Address',
Contact = 'Contact',
JournalEntry = 'JournalEntry',
JournalEntryAccount = 'JournalEntryAccount',
JournalEntrySettings = 'JournalEntrySettings',
Quotation = 'Quotation',
QuotationItem = 'QuotationItem',
QuotationSettings = 'QuotationSettings',
SalesOrder = 'SalesOrder',
SalesOrderItem = 'SalesOrderItem',
SalesOrderSettings = 'SalesOrderSettings',
Fulfillment = 'Fulfillment',
FulfillmentItem = 'FulfillmentItem',
FulfillmentSettings = 'FulfillmentSettings',
PurchaseOrder = 'PurchaseOrder',
PurchaseOrderItem = 'PurchaseOrderItem',
PurchaseOrderSettings = 'PurchaseOrderSettings',
PurchaseReceipt = 'PurchaseReceipt',
PurchaseReceiptItem = 'PurchaseReceiptItem',
PurchaseReceiptSettings = 'PurchaseReceiptSettings',
Event = 'Event',
EventSchedule = 'EventSchedule',
EventSettings = 'EventSettings',
Email = 'Email',
EmailAccount = 'EmailAccount',
PrintSettings = 'PrintSettings',
GetStarted = 'GetStarted',
}

View File

@ -35,6 +35,7 @@
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/eslint-parser": "^7.16.0",
"@types/lodash": "^4.14.179",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"@vue/cli-plugin-babel": "^4.5.0",

View File

@ -1,4 +1,6 @@
import frappe from 'frappe';
import telemetry from '../src/telemetry/telemetry';
import { Verb } from '../src/telemetry/types';
import { getSavePath, saveData, showExportInFolder } from '../src/utils';
function templateToInnerText(innerHTML) {
@ -80,13 +82,15 @@ async function exportReport(extention, reportName, getReportData) {
switch (extention) {
case 'csv':
await exportCsv(rows, columns, filePath);
return;
break;
case 'json':
await exportJson(rows, columns, filePath, filters, reportName);
return;
break;
default:
return;
}
telemetry.log(Verb.Exported, reportName, { extention });
}
export default function getCommonExportActions(reportName) {

View File

@ -25,27 +25,29 @@
>
<div id="toast-target" />
</div>
<TelemetryModal />
</div>
</template>
<script>
import './styles/index.css';
import WindowsTitleBar from '@/components/WindowsTitleBar';
import config from '@/config';
import {
connectToLocalDatabase,
postSetup,
purgeCache
} from '@/initialization';
import { IPC_ACTIONS, IPC_MESSAGES } from '@/messages';
import { ipcRenderer } from 'electron';
import frappe from 'frappe';
import fs from 'fs/promises';
import TelemetryModal from './components/once/TelemetryModal.vue';
import { showErrorDialog } from './errorHandling';
import DatabaseSelector from './pages/DatabaseSelector';
import Desk from './pages/Desk';
import SetupWizard from './pages/SetupWizard/SetupWizard';
import DatabaseSelector from './pages/DatabaseSelector';
import WindowsTitleBar from '@/components/WindowsTitleBar';
import { ipcRenderer } from 'electron';
import config from '@/config';
import { IPC_MESSAGES, IPC_ACTIONS } from '@/messages';
import {
connectToLocalDatabase,
postSetup,
purgeCache,
} from '@/initialization';
import './styles/index.css';
import { checkForUpdates, routeTo } from './utils';
import fs from 'fs/promises';
import { showErrorDialog } from './errorHandling';
export default {
name: 'App',
@ -78,6 +80,7 @@ export default {
SetupWizard,
DatabaseSelector,
WindowsTitleBar,
TelemetryModal,
},
async mounted() {
const lastSelectedFilePath = config.get('lastSelectedFilePath', null);

View File

@ -15,7 +15,7 @@ import { autoUpdater } from 'electron-updater';
import fs from 'fs/promises';
import path from 'path';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import { sendError } from './contactMothership';
import { getUrlAndTokenString, sendError } from './contactMothership';
import { getLanguageMap } from './getLanguageMap';
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from './messages';
import saveHtmlAsPdf from './saveHtmlAsPdf';
@ -261,6 +261,10 @@ ipcMain.handle(IPC_ACTIONS.GET_FILE, async (event, options) => {
return response;
});
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
return await getUrlAndTokenString();
});
/* ------------------------------
* Register autoUpdater events lis
* ------------------------------*/

View File

@ -1,6 +1,9 @@
<template>
<div>
<label class="flex items-center">
<div class="mr-3 text-gray-900 text-sm" v-if="showLabel && !labelRight">
{{ df.label }}
</div>
<div style="width: 14px; height: 14px; overflow: hidden; cursor: pointer">
<svg
v-if="checked === 1"
@ -58,7 +61,7 @@
@focus="(e) => $emit('focus', e)"
/>
</div>
<div class="ml-3 text-gray-900 text-sm" v-if="showLabel">
<div class="ml-3 text-gray-900 text-sm" v-if="showLabel && labelRight">
{{ df.label }}
</div>
</label>
@ -71,6 +74,12 @@ export default {
name: 'Check',
extends: Base,
emits: ['focus'],
props: {
labelRight: {
default: true,
type: Boolean,
},
},
data() {
return {
offBorderColor: 'rgba(17, 43, 66, 0.201322)',

View File

@ -1,22 +1,11 @@
<template>
<button
@click="openHelpLink"
class="
text-gray-900
border
px-3
py-2
flex
items-center
mb-3
z-10
bg-white
rounded-lg
text-base
"
>
<FeatherIcon class="h-6 w-6 mr-3 text-blue-400" name="help-circle" />
<button @click="openHelpLink" class="flex items-center z-10">
<p class="mr-1"><slot></slot></p>
<FeatherIcon
class="h-5 w-5 ml-3 text-blue-400"
name="help-circle"
v-if="icon"
/>
</button>
</template>
<script>
@ -27,6 +16,10 @@ import FeatherIcon from './FeatherIcon.vue';
export default {
props: {
link: String,
icon: {
default: true,
type: Boolean,
},
},
methods: {
openHelpLink() {

26
src/components/Modal.vue Normal file
View File

@ -0,0 +1,26 @@
<template>
<div
class="absolute w-screen h-screen z-20 flex justify-center items-center"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(6px)"
v-if="openModal"
>
<div
class="bg-white rounded-lg shadow-2xl"
v-bind="$attrs"
style="width: 600px"
>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
openModal: {
default: false,
type: Boolean,
},
},
};
</script>

View File

@ -0,0 +1,131 @@
<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 config, {
ConfigKeys,
telemetryOptions,
TelemetrySetting
} from '@/config';
import Button from '../Button.vue';
import FormControl from '../Controls/FormControl';
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() {
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() {
config.set(ConfigKeys.Telemetry, this.value);
this.shouldOpen = false;
},
getIsSet(value) {
return [
TelemetrySetting.allow,
TelemetrySetting.dontLogAnything,
TelemetrySetting.dontLogUsage,
].includes(value);
},
setOpen(telemetry) {
const openCount = config.get(ConfigKeys.OpenCount);
this.shouldOpen = !this.getIsSet(telemetry) && openCount >= 4;
},
},
mounted() {
const telemetry = config.get(ConfigKeys.Telemetry);
this.setOpen(telemetry);
this.value = telemetry;
},
};
</script>

View File

@ -1,4 +0,0 @@
import Store from 'electron-store';
let config = new Store();
export default config;

32
src/config.ts Normal file
View File

@ -0,0 +1,32 @@
import Store from 'electron-store';
import frappe from 'frappe';
const config = new Store();
export default config;
export enum ConfigKeys {
Files = 'files',
LastSelectedFilePath = 'lastSelectedFilePath',
Language = 'language',
DeviceId = 'deviceId',
Telemetry = 'telemetry',
OpenCount = 'openCount',
}
export enum TelemetrySetting {
allow = 'allow',
dontLogUsage = 'dontLogUsage',
dontLogAnything = 'dontLogAnything',
}
export const telemetryOptions = {
[TelemetrySetting.allow]: frappe.t`Allow Telemetry`,
[TelemetrySetting.dontLogUsage]: frappe.t`Don't Log Usage`,
[TelemetrySetting.dontLogAnything]: frappe.t`Don't Log Anything`,
};
export interface ConfigFile {
id: string;
companyName: string;
filePath: string;
}

View File

@ -4,24 +4,25 @@ import http from 'http';
import https from 'https';
import path from 'path';
function getUrlAndTokenString() {
export function getUrlAndTokenString() {
const inProduction = app.isPackaged;
const empty = { url: '', telemetryUrl: '', tokenString: '' };
let errLogCredsPath = path.join(
process.resourcesPath,
'../creds/err_log_creds.txt'
'../creds/log_creds.txt'
);
if (!fs.existsSync(errLogCredsPath)) {
errLogCredsPath = path.join(__dirname, '../err_log_creds.txt');
errLogCredsPath = path.join(__dirname, '../log_creds.txt');
}
if (!fs.existsSync(errLogCredsPath)) {
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
return;
return empty;
}
let apiKey, apiSecret, url;
let apiKey, apiSecret, url, telemetryUrl;
try {
[apiKey, apiSecret, url] = fs
[apiKey, apiSecret, url, telemetryUrl] = fs
.readFileSync(errLogCredsPath, 'utf-8')
.split('\n')
.filter((f) => f.length);
@ -30,10 +31,14 @@ function getUrlAndTokenString() {
console.log(`logging error using creds at: ${errLogCredsPath} failed`);
console.log(err);
}
return;
return empty;
}
return { url: encodeURI(url), tokenString: `token ${apiKey}:${apiSecret}` };
return {
url: encodeURI(url),
telemetryUrl: encodeURI(telemetryUrl),
tokenString: `token ${apiKey}:${apiSecret}`,
};
}
function post(bodyJson) {

View File

@ -2,6 +2,8 @@ import { Doc, Field, FieldType, Map } from '@/types/model';
import frappe from 'frappe';
import { isNameAutoSet } from 'frappe/model/naming';
import { parseCSV } from './csvParser';
import telemetry from './telemetry/telemetry';
import { Noun, Verb } from './telemetry/types';
export const importable = [
'SalesInvoice',
@ -387,6 +389,12 @@ export class Importer {
setLoadingStatus(true, entriesMade, docObjs.length);
} catch (err) {
setLoadingStatus(false, entriesMade, docObjs.length);
telemetry.log(Verb.Imported, this.doctype as Noun, {
success: false,
count: entriesMade,
});
return this.handleError(doc, err as Error, status);
}
@ -395,6 +403,11 @@ export class Importer {
setLoadingStatus(false, entriesMade, docObjs.length);
status.success = true;
telemetry.log(Verb.Imported, this.doctype as Noun, {
success: true,
count: entriesMade,
});
return status;
}

View File

@ -7,7 +7,9 @@ import {
ValidationError,
} from 'frappe/common/errors';
import BaseDocument from 'frappe/model/document';
import config, { ConfigKeys, TelemetrySetting } from './config';
import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
import telemetry from './telemetry/telemetry';
import { showMessageDialog, showToast } from './utils';
interface ErrorLog {
@ -17,6 +19,11 @@ interface ErrorLog {
more?: object;
}
function getCanLog(): boolean {
const telemetrySetting = config.get(ConfigKeys.Telemetry);
return telemetrySetting !== TelemetrySetting.dontLogAnything;
}
function shouldNotStore(error: Error) {
return [MandatoryError, ValidationError].some(
(errorClass) => error instanceof errorClass
@ -38,14 +45,14 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
cb?.();
}
function getToastProps(errorLogObj: ErrorLog, cb?: Function) {
function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) {
const props = {
message: t`Error: ` + errorLogObj.name,
type: 'error',
};
// @ts-ignore
if (!frappe.SystemSettings?.autoReportErrors) {
if (!canLog) {
Object.assign(props, {
actionText: t`Report Error`,
action: () => {
@ -74,6 +81,7 @@ export function handleError(
more: object = {},
cb?: Function
) {
telemetry.error(error.name);
if (shouldLog) {
console.error(error);
}
@ -85,10 +93,11 @@ export function handleError(
const errorLogObj = getErrorLogObject(error, more);
// @ts-ignore
if (frappe.SystemSettings?.autoReportErrors) {
const canLog = getCanLog();
if (canLog) {
reportError(errorLogObj, cb);
} else {
showToast(getToastProps(errorLogObj, cb));
showToast(getToastProps(errorLogObj, canLog, cb));
}
}

View File

@ -1,12 +1,15 @@
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 } from './messages';
import { DB_CONN_FAILURE, IPC_ACTIONS } from './messages';
import runMigrate from './migrate';
import { callInitializeMoneyMaker, getSavePath, setLanguageMap } from './utils';
import { getId } from './telemetry/helpers';
import telemetry from './telemetry/telemetry';
import { callInitializeMoneyMaker, getSavePath } from './utils';
export async function createNewDatabase() {
const { canceled, filePath } = await getSavePath('books', 'db');
@ -81,6 +84,7 @@ export async function connectToLocalDatabase(filePath) {
files = [
{
companyName,
id: getId(),
filePath,
},
...files.filter((file) => file.filePath !== filePath),
@ -93,6 +97,11 @@ 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();
return { connectionSuccess: true, reason: '' };
}

View File

@ -4,10 +4,11 @@ import { createApp } from 'vue';
import models from '../models';
import App from './App';
import FeatherIcon from './components/FeatherIcon';
import config from './config';
import config, { ConfigKeys } from './config';
import { getErrorHandled, handleError } from './errorHandling';
import { IPC_CHANNELS, IPC_MESSAGES } from './messages';
import router from './router';
import telemetry from './telemetry/telemetry';
import { outsideClickDirective } from './ui';
import { setLanguageMap, showToast, stringifyCircular } from './utils';
(async () => {
@ -16,6 +17,10 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
await setLanguageMap(language);
}
if (process.env.NODE_ENV === 'development') {
window.config = config;
}
frappe.isServer = true;
frappe.isElectron = true;
frappe.initializeAndRegister(models, language);
@ -25,7 +30,6 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke);
window.frappe = frappe;
window.frappe.store = {};
window.onerror = (message, source, lineno, colno, error) => {
error = error ?? new Error('triggered in window.onerror');
@ -85,9 +89,21 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
console.error(err, vm, info);
};
incrementOpenCount();
app.mount('body');
})();
function incrementOpenCount() {
let openCount = config.get(ConfigKeys.OpenCount);
if (typeof openCount !== 'number') {
openCount = 1;
} else {
openCount += 1;
}
config.set(ConfigKeys.OpenCount, openCount);
}
function registerIpcRendererListeners() {
ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => {
Object.assign(window.frappe.store, message);
@ -136,4 +152,13 @@ function registerIpcRendererListeners() {
error.name = 'Updation Error';
handleError(true, error);
});
document.addEventListener('visibilitychange', function () {
if (document.visibilityState !== 'hidden') {
return;
}
const { url, data } = telemetry.stop();
navigator.sendBeacon(url, data);
});
}

View File

@ -26,6 +26,7 @@ export const IPC_ACTIONS = {
GET_LANGUAGE_MAP: 'get-language-map',
CHECK_FOR_UPDATES: 'check-for-updates',
GET_FILE: 'get-file',
GET_CREDS: 'get-creds',
};
// ipcMain.send(...)

View File

@ -320,8 +320,11 @@
v-if="!importType"
class="flex justify-center h-full w-full items-center mb-16"
>
<HowTo link="https://youtu.be/ukHAgcnVxTQ">
{{ t`How to Use Data Import?` }}
<HowTo
link="https://youtu.be/ukHAgcnVxTQ"
class="text-gray-900 rounded-lg text-base border px-3 py-2"
>
{{ t`How to Use Data Import` }}
</HowTo>
</div>
<Loading

View File

@ -51,15 +51,17 @@
</div>
</template>
<script>
import frappe from 'frappe';
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import Button from '@/components/Button';
import BackLink from '@/components/BackLink';
import TwoColumnForm from '@/components/TwoColumnForm';
import { IPC_ACTIONS } from '@/messages';
import telemetry from '@/telemetry/telemetry';
import { Verb } from '@/telemetry/types';
import { makePDF } from '@/utils';
import { ipcRenderer } from 'electron';
import { IPC_ACTIONS } from '@/messages';
import frappe from 'frappe';
export default {
name: 'PrintView',
@ -96,6 +98,7 @@ export default {
if (!savePath) return;
const html = this.$refs.printContainer.innerHTML;
telemetry.log(Verb.Exported, 'SalesInvoice', { extension: 'pdf' });
makePDF(html, savePath);
},
async getSavePath() {

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="flex flex-col justify-between h-full">
<TwoColumnForm
v-if="doc"
:doc="doc"
@ -8,10 +8,27 @@
:emit-change="true"
@change="forwardChangeEvent"
/>
<div class="flex flex-row justify-between my-4">
<LanguageSelector class="text-sm" input-class="px-4 py-1.5"/>
<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>
<button
class="text-gray-900 text-sm hover:bg-gray-200 rounded-md px-4 py-1.5"
class="
text-gray-900 text-sm
bg-gray-100
hover:bg-gray-200
rounded-md
px-4
py-1.5
"
@click="checkForUpdates(true)"
>
Check for Updates
@ -21,14 +38,17 @@
</template>
<script>
import frappe from 'frappe';
import TwoColumnForm from '@/components/TwoColumnForm';
import { checkForUpdates } from '@/utils';
import FormControl from '@/components/Controls/FormControl';
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
import TwoColumnForm from '@/components/TwoColumnForm';
import config, { ConfigKeys, telemetryOptions } from '@/config';
import { checkForUpdates } from '@/utils';
import frappe from 'frappe';
export default {
name: 'TabSystem',
components: {
FormControl,
TwoColumnForm,
LanguageSelector,
},
@ -36,13 +56,27 @@ export default {
data() {
return {
doc: null,
telemetry: '',
};
},
async mounted() {
this.doc = frappe.SystemSettings;
this.companyName = frappe.AccountingSettings.companyName;
this.telemetry = config.get(ConfigKeys.Telemetry);
},
computed: {
df() {
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() {
let meta = frappe.getMeta('SystemSettings');
return meta.getQuickEditFields();
@ -50,6 +84,10 @@ export default {
},
methods: {
checkForUpdates,
setValue(value) {
this.telemetry = value;
config.set(ConfigKeys.Telemetry, value);
},
forwardChangeEvent(...args) {
this.$emit('change', ...args);
},

View File

@ -5,6 +5,7 @@ import countryList from '~/fixtures/countryInfo.json';
import importCharts from '../../../accounting/importCOA';
import generateTaxes from '../../../models/doctype/Tax/RegionalEntries';
import regionalModelUpdates from '../../../models/regionalModelUpdates';
import { getId } from '../../telemetry/helpers';
import { callInitializeMoneyMaker } from '../../utils';
export default async function setupCompany(setupWizardValues) {
@ -116,6 +117,7 @@ function updateInitializationConfig(language) {
files.forEach((file) => {
if (file.filePath === filePath) {
file.companyName = frappe.AccountingSettings.companyName;
file.id = getId();
}
});
config.set('files', files);

View File

@ -12,6 +12,8 @@ import QuickEditForm from '@/pages/QuickEditForm';
import Report from '@/pages/Report';
import Settings from '@/pages/Settings/Settings';
import { createRouter, createWebHistory } from 'vue-router';
import telemetry from './telemetry/telemetry';
import { NounEnum, Verb } from './telemetry/types';
const routes = [
{
@ -114,6 +116,28 @@ const routes = [
let router = createRouter({ routes, history: createWebHistory() });
function removeDetails(path) {
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),
};
telemetry.log(Verb.Navigated, NounEnum.Route, more);
});
if (process.env.NODE_ENV === 'development') {
window.router = router;
}

124
src/telemetry/helpers.ts Normal file
View File

@ -0,0 +1,124 @@
import config, { ConfigFile, ConfigKeys } from '@/config';
import { DoctypeName } from '../../models/types';
import { Count, Locale, 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 getLocale(): Locale {
// @ts-ignore
const country: string = frappe.AccountingSettings?.country ?? '';
const language: string = config.get('language') as string;
return { country, language };
}
export async function getCounts(): Promise<Count> {
const interestingDocs = [
DoctypeName.Payment,
DoctypeName.PaymentFor,
DoctypeName.SalesInvoice,
DoctypeName.SalesInvoiceItem,
DoctypeName.PurchaseInvoice,
DoctypeName.PurchaseInvoiceItem,
DoctypeName.JournalEntry,
DoctypeName.JournalEntryAccount,
DoctypeName.Account,
DoctypeName.Tax,
];
const countMap: Count = {};
type CountResponse = { 'count(*)': number }[];
for (const name of interestingDocs) {
// @ts-ignore
const queryResponse: CountResponse = await frappe.db.knex(name).count();
const count: number = queryResponse[0]['count(*)'];
countMap[name] = count;
}
// @ts-ignore
const supplierCount: CountResponse = await frappe.db
.knex('Party')
.count()
.where({ supplier: 1 });
// @ts-ignore
const customerCount: CountResponse = await frappe.db
.knex('Party')
.count()
.where({ customer: 1 });
countMap[DoctypeName.Customer] = customerCount[0]['count(*)'];
countMap[DoctypeName.Supplier] = supplierCount[0]['count(*)'];
return countMap;
}
export function getDeviceId(): UniqueId {
let deviceId = config.get(ConfigKeys.DeviceId) as string | undefined;
if (deviceId === undefined) {
deviceId = getId();
config.set(ConfigKeys.DeviceId, deviceId);
}
return deviceId;
}
export function getInstanceId(): UniqueId {
const files = config.get(ConfigKeys.Files) as ConfigFile[];
// @ts-ignore
const companyName = frappe.AccountingSettings?.companyName;
if (companyName === undefined) {
return '';
}
const file = files.find((f) => f.companyName === companyName);
if (file === undefined) {
return addNewFile(companyName, files);
}
if (file.id === undefined) {
return setInstanceId(companyName, files);
}
return file.id;
}
function addNewFile(companyName: string, files: ConfigFile[]): UniqueId {
const newFile: ConfigFile = {
companyName,
filePath: config.get(ConfigKeys.LastSelectedFilePath, '') as string,
id: getId(),
};
files.push(newFile);
config.set(ConfigKeys.Files, files);
return newFile.id;
}
function setInstanceId(companyName: string, files: ConfigFile[]): UniqueId {
let id = '';
for (const file of files) {
if (file.id) {
continue;
}
file.id = getId();
if (file.companyName === companyName) {
id = file.id;
}
}
config.set(ConfigKeys.Files, files);
return id;
}

View File

@ -0,0 +1,93 @@
import config, { ConfigKeys, TelemetrySetting } from '@/config';
import frappe from 'frappe';
import { cloneDeep } from 'lodash';
import { getCounts, getDeviceId, getInstanceId, getLocale } from './helpers';
import { Noun, Telemetry, Verb } from './types';
class TelemetryManager {
#url: string = '';
#token: string = '';
#started = false;
#telemetryObject: Partial<Telemetry> = {};
start() {
this.#telemetryObject.locale = getLocale();
this.#telemetryObject.deviceId ||= getDeviceId();
this.#telemetryObject.instanceId ||= getInstanceId();
this.#telemetryObject.openTime ||= new Date().valueOf();
this.#telemetryObject.timeline ??= [];
this.#telemetryObject.errors ??= {};
this.#started = true;
}
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<string, unknown>) {
if (!this.#started) {
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;
}
async setCount() {
this.#telemetryObject.counts = this.getCanLog() ? await getCounts() : {};
}
stop() {
// Will set ids if not set.
this.start();
//@ts-ignore
this.#telemetryObject.version = frappe.store.appVersion ?? '';
this.#telemetryObject.closeTime = new Date().valueOf();
const telemetryObject = this.#telemetryObject;
this.#started = false;
this.#telemetryObject = {};
if (config.get(ConfigKeys.Telemetry) === TelemetrySetting.dontLogAnything) {
return '';
}
const data = JSON.stringify({
token: this.#token,
telemetryData: telemetryObject,
});
return { url: this.#url, data };
}
get telemetryObject(): Readonly<Partial<Telemetry>> {
return cloneDeep(this.#telemetryObject);
}
}
export default new TelemetryManager();

47
src/telemetry/types.ts Normal file
View File

@ -0,0 +1,47 @@
import { DoctypeName } from 'models/types';
export type AppVersion = string;
export type UniqueId = string;
export type Timestamp = number;
export interface InteractionEvent {
time: Timestamp;
verb: Verb;
noun: Noun;
more?: Record<string, unknown>;
}
export interface Locale {
country: string;
language: string;
}
export type Count = Partial<{
[key in DoctypeName]: number;
}>;
export interface Telemetry {
deviceId: UniqueId;
instanceId: UniqueId;
openTime: Timestamp;
closeTime: Timestamp;
timeline?: InteractionEvent[];
counts?: Count;
errors: Record<string, number>;
locale: Locale;
version: AppVersion;
}
export enum Verb {
Created = 'created',
Deleted = 'deleted',
Navigated = 'navigated',
Imported = 'imported',
Exported = 'exported',
}
export enum NounEnum {
Route = 'route',
}
export type Noun = string | NounEnum;

View File

@ -1296,6 +1296,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/lodash@^4.14.179":
version "4.14.179"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
"@types/mime@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"