2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 11:29:03 +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_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }} ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }} ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: | run: |
echo $ERR_LOG_KEY > err_log_creds.txt echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build - name: Run build
env: env:
@ -70,10 +72,12 @@ jobs:
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }} ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }} ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }} ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: | run: |
echo $ERR_LOG_KEY > err_log_creds.txt echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build - name: Run build
env: env:
@ -112,10 +116,12 @@ jobs:
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }} ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }} ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }} ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
run: | run: |
echo $ERR_LOG_KEY > err_log_creds.txt echo $ERR_LOG_KEY > log_creds.txt
echo $ERR_LOG_SECRET >> err_log_creds.txt echo $ERR_LOG_SECRET >> log_creds.txt
echo $ERR_LOG_URL >> err_log_creds.txt echo $ERR_LOG_URL >> log_creds.txt
echo $TELEMETRY_URL >> log_creds.txt
- name: Run build - name: Run build
env: env:

2
.gitignore vendored
View File

@ -24,4 +24,4 @@ yarn-error.log*
#Electron-builder output #Electron-builder output
/dist_electron /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. 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). 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. 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. 🙂 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. 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 afterSign: build/notarize.js
extraResources: [ extraResources: [
{ {
from: 'err_log_creds.txt', from: 'log_creds.txt',
to: '../creds/err_log_creds.txt', to: '../creds/log_creds.txt',
} }
] ]
mac: mac:

View File

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

View File

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

View File

@ -84,19 +84,11 @@ module.exports = {
default: 0, default: 0,
description: t`Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.`, 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: [ quickEditFields: [
'locale', 'locale',
'dateFormat', 'dateFormat',
'displayPrecision', 'displayPrecision',
'hideGetStarted', '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": { "devDependencies": {
"@babel/core": "^7.16.0", "@babel/core": "^7.16.0",
"@babel/eslint-parser": "^7.16.0", "@babel/eslint-parser": "^7.16.0",
"@types/lodash": "^4.14.179",
"@typescript-eslint/eslint-plugin": "^4.15.1", "@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1", "@typescript-eslint/parser": "^4.15.1",
"@vue/cli-plugin-babel": "^4.5.0", "@vue/cli-plugin-babel": "^4.5.0",

View File

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

View File

@ -25,27 +25,29 @@
> >
<div id="toast-target" /> <div id="toast-target" />
</div> </div>
<TelemetryModal />
</div> </div>
</template> </template>
<script> <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 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 Desk from './pages/Desk';
import SetupWizard from './pages/SetupWizard/SetupWizard'; import SetupWizard from './pages/SetupWizard/SetupWizard';
import DatabaseSelector from './pages/DatabaseSelector'; import './styles/index.css';
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 { checkForUpdates, routeTo } from './utils'; import { checkForUpdates, routeTo } from './utils';
import fs from 'fs/promises';
import { showErrorDialog } from './errorHandling';
export default { export default {
name: 'App', name: 'App',
@ -78,6 +80,7 @@ export default {
SetupWizard, SetupWizard,
DatabaseSelector, DatabaseSelector,
WindowsTitleBar, WindowsTitleBar,
TelemetryModal,
}, },
async mounted() { async mounted() {
const lastSelectedFilePath = config.get('lastSelectedFilePath', null); const lastSelectedFilePath = config.get('lastSelectedFilePath', null);

View File

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

View File

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

View File

@ -1,22 +1,11 @@
<template> <template>
<button <button @click="openHelpLink" class="flex items-center z-10">
@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" />
<p class="mr-1"><slot></slot></p> <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> </button>
</template> </template>
<script> <script>
@ -27,6 +16,10 @@ import FeatherIcon from './FeatherIcon.vue';
export default { export default {
props: { props: {
link: String, link: String,
icon: {
default: true,
type: Boolean,
},
}, },
methods: { methods: {
openHelpLink() { 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 https from 'https';
import path from 'path'; import path from 'path';
function getUrlAndTokenString() { export function getUrlAndTokenString() {
const inProduction = app.isPackaged; const inProduction = app.isPackaged;
const empty = { url: '', telemetryUrl: '', tokenString: '' };
let errLogCredsPath = path.join( let errLogCredsPath = path.join(
process.resourcesPath, process.resourcesPath,
'../creds/err_log_creds.txt' '../creds/log_creds.txt'
); );
if (!fs.existsSync(errLogCredsPath)) { if (!fs.existsSync(errLogCredsPath)) {
errLogCredsPath = path.join(__dirname, '../err_log_creds.txt'); errLogCredsPath = path.join(__dirname, '../log_creds.txt');
} }
if (!fs.existsSync(errLogCredsPath)) { if (!fs.existsSync(errLogCredsPath)) {
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`); !inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
return; return empty;
} }
let apiKey, apiSecret, url; let apiKey, apiSecret, url, telemetryUrl;
try { try {
[apiKey, apiSecret, url] = fs [apiKey, apiSecret, url, telemetryUrl] = fs
.readFileSync(errLogCredsPath, 'utf-8') .readFileSync(errLogCredsPath, 'utf-8')
.split('\n') .split('\n')
.filter((f) => f.length); .filter((f) => f.length);
@ -30,10 +31,14 @@ function getUrlAndTokenString() {
console.log(`logging error using creds at: ${errLogCredsPath} failed`); console.log(`logging error using creds at: ${errLogCredsPath} failed`);
console.log(err); 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) { function post(bodyJson) {

View File

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

View File

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

View File

@ -1,12 +1,15 @@
import config from '@/config'; import config from '@/config';
import { ipcRenderer } from 'electron';
import SQLiteDatabase from 'frappe/backends/sqlite'; import SQLiteDatabase from 'frappe/backends/sqlite';
import fs from 'fs'; import fs from 'fs';
import models from '../models'; import models from '../models';
import regionalModelUpdates from '../models/regionalModelUpdates'; import regionalModelUpdates from '../models/regionalModelUpdates';
import postStart, { setCurrencySymbols } from '../server/postStart'; 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 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() { export async function createNewDatabase() {
const { canceled, filePath } = await getSavePath('books', 'db'); const { canceled, filePath } = await getSavePath('books', 'db');
@ -81,6 +84,7 @@ export async function connectToLocalDatabase(filePath) {
files = [ files = [
{ {
companyName, companyName,
id: getId(),
filePath, filePath,
}, },
...files.filter((file) => file.filePath !== filePath), ...files.filter((file) => file.filePath !== filePath),
@ -93,6 +97,11 @@ export async function connectToLocalDatabase(filePath) {
// second init with currency, normal usage // second init with currency, normal usage
await callInitializeMoneyMaker(); 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: '' }; return { connectionSuccess: true, reason: '' };
} }

View File

@ -4,10 +4,11 @@ import { createApp } from 'vue';
import models from '../models'; import models from '../models';
import App from './App'; import App from './App';
import FeatherIcon from './components/FeatherIcon'; import FeatherIcon from './components/FeatherIcon';
import config from './config'; import config, { ConfigKeys } from './config';
import { getErrorHandled, handleError } from './errorHandling'; import { getErrorHandled, handleError } from './errorHandling';
import { IPC_CHANNELS, IPC_MESSAGES } from './messages'; import { IPC_CHANNELS, IPC_MESSAGES } from './messages';
import router from './router'; import router from './router';
import telemetry from './telemetry/telemetry';
import { outsideClickDirective } from './ui'; import { outsideClickDirective } from './ui';
import { setLanguageMap, showToast, stringifyCircular } from './utils'; import { setLanguageMap, showToast, stringifyCircular } from './utils';
(async () => { (async () => {
@ -16,6 +17,10 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
await setLanguageMap(language); await setLanguageMap(language);
} }
if (process.env.NODE_ENV === 'development') {
window.config = config;
}
frappe.isServer = true; frappe.isServer = true;
frappe.isElectron = true; frappe.isElectron = true;
frappe.initializeAndRegister(models, language); frappe.initializeAndRegister(models, language);
@ -25,7 +30,6 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke); ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke);
window.frappe = frappe; window.frappe = frappe;
window.frappe.store = {};
window.onerror = (message, source, lineno, colno, error) => { window.onerror = (message, source, lineno, colno, error) => {
error = error ?? new Error('triggered in window.onerror'); error = error ?? new Error('triggered in window.onerror');
@ -85,9 +89,21 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
console.error(err, vm, info); console.error(err, vm, info);
}; };
incrementOpenCount();
app.mount('body'); 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() { function registerIpcRendererListeners() {
ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => { ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => {
Object.assign(window.frappe.store, message); Object.assign(window.frappe.store, message);
@ -136,4 +152,13 @@ function registerIpcRendererListeners() {
error.name = 'Updation Error'; error.name = 'Updation Error';
handleError(true, 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', GET_LANGUAGE_MAP: 'get-language-map',
CHECK_FOR_UPDATES: 'check-for-updates', CHECK_FOR_UPDATES: 'check-for-updates',
GET_FILE: 'get-file', GET_FILE: 'get-file',
GET_CREDS: 'get-creds',
}; };
// ipcMain.send(...) // ipcMain.send(...)

View File

@ -320,8 +320,11 @@
v-if="!importType" v-if="!importType"
class="flex justify-center h-full w-full items-center mb-16" class="flex justify-center h-full w-full items-center mb-16"
> >
<HowTo link="https://youtu.be/ukHAgcnVxTQ"> <HowTo
{{ t`How to Use Data Import?` }} 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> </HowTo>
</div> </div>
<Loading <Loading

View File

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

View File

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

View File

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

View File

@ -12,6 +12,8 @@ import QuickEditForm from '@/pages/QuickEditForm';
import Report from '@/pages/Report'; import Report from '@/pages/Report';
import Settings from '@/pages/Settings/Settings'; import Settings from '@/pages/Settings/Settings';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import telemetry from './telemetry/telemetry';
import { NounEnum, Verb } from './telemetry/types';
const routes = [ const routes = [
{ {
@ -114,6 +116,28 @@ const routes = [
let router = createRouter({ routes, history: createWebHistory() }); 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') { if (process.env.NODE_ENV === 'development') {
window.router = router; 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" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== 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": "@types/mime@^1":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"