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:
commit
ac28496619
24
.github/workflows/publish.yml
vendored
24
.github/workflows/publish.yml
vendored
@ -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
2
.gitignore
vendored
@ -24,4 +24,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
/dist_electron
|
/dist_electron
|
||||||
err_log_creds.txt
|
log_creds.txt
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -383,4 +383,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
t,
|
t,
|
||||||
T,
|
T,
|
||||||
|
store: {},
|
||||||
};
|
};
|
||||||
|
@ -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) {
|
||||||
|
@ -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
53
models/types.ts
Normal 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',
|
||||||
|
}
|
@ -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",
|
||||||
|
@ -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) {
|
||||||
|
29
src/App.vue
29
src/App.vue
@ -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);
|
||||||
|
@ -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
|
||||||
* ------------------------------*/
|
* ------------------------------*/
|
||||||
|
@ -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)',
|
||||||
|
@ -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
26
src/components/Modal.vue
Normal 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>
|
131
src/components/once/TelemetryModal.vue
Normal file
131
src/components/once/TelemetryModal.vue
Normal 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>
|
@ -1,4 +0,0 @@
|
|||||||
import Store from 'electron-store';
|
|
||||||
|
|
||||||
let config = new Store();
|
|
||||||
export default config;
|
|
32
src/config.ts
Normal file
32
src/config.ts
Normal 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;
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
29
src/main.js
29
src/main.js
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -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(...)
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
|
@ -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
124
src/telemetry/helpers.ts
Normal 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;
|
||||||
|
}
|
93
src/telemetry/telemetry.ts
Normal file
93
src/telemetry/telemetry.ts
Normal 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
47
src/telemetry/types.ts
Normal 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;
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user