mirror of
https://github.com/frappe/books.git
synced 2024-12-23 03:19:01 +00:00
Merge pull request #364 from 18alantom/opt-in-telem
chore: add anon opt in telemetry
This commit is contained in:
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_URL: ${{ secrets.ERR_LOG_URL }}
|
||||
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
|
||||
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
|
||||
run: |
|
||||
echo $ERR_LOG_KEY > err_log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> err_log_creds.txt
|
||||
echo $ERR_LOG_URL >> err_log_creds.txt
|
||||
echo $ERR_LOG_KEY > log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> log_creds.txt
|
||||
echo $ERR_LOG_URL >> log_creds.txt
|
||||
echo $TELEMETRY_URL >> log_creds.txt
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
@ -70,10 +72,12 @@ jobs:
|
||||
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
|
||||
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
|
||||
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
|
||||
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
|
||||
run: |
|
||||
echo $ERR_LOG_KEY > err_log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> err_log_creds.txt
|
||||
echo $ERR_LOG_URL >> err_log_creds.txt
|
||||
echo $ERR_LOG_KEY > log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> log_creds.txt
|
||||
echo $ERR_LOG_URL >> log_creds.txt
|
||||
echo $TELEMETRY_URL >> log_creds.txt
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
@ -112,10 +116,12 @@ jobs:
|
||||
ERR_LOG_KEY: ${{ secrets.ERR_LOG_KEY }}
|
||||
ERR_LOG_URL: ${{ secrets.ERR_LOG_URL }}
|
||||
ERR_LOG_SECRET: ${{ secrets.ERR_LOG_SECRET }}
|
||||
TELEMETRY_URL: ${{ secrets.TELEMETRY_URL }}
|
||||
run: |
|
||||
echo $ERR_LOG_KEY > err_log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> err_log_creds.txt
|
||||
echo $ERR_LOG_URL >> err_log_creds.txt
|
||||
echo $ERR_LOG_KEY > log_creds.txt
|
||||
echo $ERR_LOG_SECRET >> log_creds.txt
|
||||
echo $ERR_LOG_URL >> log_creds.txt
|
||||
echo $TELEMETRY_URL >> log_creds.txt
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -24,4 +24,4 @@ yarn-error.log*
|
||||
|
||||
#Electron-builder output
|
||||
/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.
|
||||
2. You can help us with language support by [contributing translations](https://github.com/frappe/books/wiki/Contributing-Translations).
|
||||
3. You report errors by setting **Hide & Auto Report Errors** in _Settings > System_.
|
||||
3. If you're a user, you can switch on [anonymized telemetry](https://github.com/frappe/books/wiki/Anonymized-Opt-In-Telemetry).
|
||||
4. If you're an ardent user you can tell us what you would like to see.
|
||||
5. If you have accounting requirements, you can become an ardent user. 🙂
|
||||
6. You can join our [telegram group](https://t.me/frappebooks) and share your thoughts.
|
||||
|
@ -3,8 +3,8 @@ appId: io.frappe.books
|
||||
afterSign: build/notarize.js
|
||||
extraResources: [
|
||||
{
|
||||
from: 'err_log_creds.txt',
|
||||
to: '../creds/err_log_creds.txt',
|
||||
from: 'log_creds.txt',
|
||||
to: '../creds/log_creds.txt',
|
||||
}
|
||||
]
|
||||
mac:
|
||||
|
@ -383,4 +383,5 @@ module.exports = {
|
||||
},
|
||||
t,
|
||||
T,
|
||||
store: {},
|
||||
};
|
||||
|
@ -3,6 +3,8 @@ const Observable = require('frappe/utils/observable');
|
||||
const naming = require('./naming');
|
||||
const { isPesa } = require('../utils/index');
|
||||
const { DEFAULT_INTERNAL_PRECISION } = require('../utils/consts');
|
||||
const { Verb } = require('@/telemetry/types');
|
||||
const { default: telemetry } = require('@/telemetry/telemetry');
|
||||
|
||||
module.exports = class BaseDocument extends Observable {
|
||||
constructor(data) {
|
||||
@ -577,6 +579,7 @@ module.exports = class BaseDocument extends Observable {
|
||||
await this.trigger('afterInsert');
|
||||
await this.trigger('afterSave');
|
||||
|
||||
telemetry.log(Verb.Created, this.doctype);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -620,6 +623,8 @@ module.exports = class BaseDocument extends Observable {
|
||||
await this.trigger('beforeDelete');
|
||||
await frappe.db.delete(this.doctype, this.name);
|
||||
await this.trigger('afterDelete');
|
||||
|
||||
telemetry.log(Verb.Deleted, this.doctype);
|
||||
}
|
||||
|
||||
async submitOrRevert(isSubmit) {
|
||||
|
@ -84,19 +84,11 @@ module.exports = {
|
||||
default: 0,
|
||||
description: t`Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.`,
|
||||
},
|
||||
{
|
||||
fieldname: 'autoReportErrors',
|
||||
label: t`Hide & Auto Report Errors`,
|
||||
fieldtype: 'Check',
|
||||
default: 0,
|
||||
description: t`Prevent errors from showing and automatically report all errors.`,
|
||||
},
|
||||
],
|
||||
quickEditFields: [
|
||||
'locale',
|
||||
'dateFormat',
|
||||
'displayPrecision',
|
||||
'hideGetStarted',
|
||||
'autoReportErrors',
|
||||
],
|
||||
};
|
||||
|
53
models/types.ts
Normal file
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": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.1",
|
||||
"@typescript-eslint/parser": "^4.15.1",
|
||||
"@vue/cli-plugin-babel": "^4.5.0",
|
||||
|
@ -1,4 +1,6 @@
|
||||
import frappe from 'frappe';
|
||||
import telemetry from '../src/telemetry/telemetry';
|
||||
import { Verb } from '../src/telemetry/types';
|
||||
import { getSavePath, saveData, showExportInFolder } from '../src/utils';
|
||||
|
||||
function templateToInnerText(innerHTML) {
|
||||
@ -80,13 +82,15 @@ async function exportReport(extention, reportName, getReportData) {
|
||||
switch (extention) {
|
||||
case 'csv':
|
||||
await exportCsv(rows, columns, filePath);
|
||||
return;
|
||||
break;
|
||||
case 'json':
|
||||
await exportJson(rows, columns, filePath, filters, reportName);
|
||||
return;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
telemetry.log(Verb.Exported, reportName, { extention });
|
||||
}
|
||||
|
||||
export default function getCommonExportActions(reportName) {
|
||||
|
29
src/App.vue
29
src/App.vue
@ -25,27 +25,29 @@
|
||||
>
|
||||
<div id="toast-target" />
|
||||
</div>
|
||||
<TelemetryModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import './styles/index.css';
|
||||
import WindowsTitleBar from '@/components/WindowsTitleBar';
|
||||
import config from '@/config';
|
||||
import {
|
||||
connectToLocalDatabase,
|
||||
postSetup,
|
||||
purgeCache
|
||||
} from '@/initialization';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from '@/messages';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import frappe from 'frappe';
|
||||
import fs from 'fs/promises';
|
||||
import TelemetryModal from './components/once/TelemetryModal.vue';
|
||||
import { showErrorDialog } from './errorHandling';
|
||||
import DatabaseSelector from './pages/DatabaseSelector';
|
||||
import Desk from './pages/Desk';
|
||||
import SetupWizard from './pages/SetupWizard/SetupWizard';
|
||||
import DatabaseSelector from './pages/DatabaseSelector';
|
||||
import WindowsTitleBar from '@/components/WindowsTitleBar';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import config from '@/config';
|
||||
import { IPC_MESSAGES, IPC_ACTIONS } from '@/messages';
|
||||
import {
|
||||
connectToLocalDatabase,
|
||||
postSetup,
|
||||
purgeCache,
|
||||
} from '@/initialization';
|
||||
import './styles/index.css';
|
||||
import { checkForUpdates, routeTo } from './utils';
|
||||
import fs from 'fs/promises';
|
||||
import { showErrorDialog } from './errorHandling';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
@ -78,6 +80,7 @@ export default {
|
||||
SetupWizard,
|
||||
DatabaseSelector,
|
||||
WindowsTitleBar,
|
||||
TelemetryModal,
|
||||
},
|
||||
async mounted() {
|
||||
const lastSelectedFilePath = config.get('lastSelectedFilePath', null);
|
||||
|
@ -15,7 +15,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||
import { sendError } from './contactMothership';
|
||||
import { getUrlAndTokenString, sendError } from './contactMothership';
|
||||
import { getLanguageMap } from './getLanguageMap';
|
||||
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||
import saveHtmlAsPdf from './saveHtmlAsPdf';
|
||||
@ -261,6 +261,10 @@ ipcMain.handle(IPC_ACTIONS.GET_FILE, async (event, options) => {
|
||||
return response;
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
|
||||
return await getUrlAndTokenString();
|
||||
});
|
||||
|
||||
/* ------------------------------
|
||||
* Register autoUpdater events lis
|
||||
* ------------------------------*/
|
||||
|
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<div class="mr-3 text-gray-900 text-sm" v-if="showLabel && !labelRight">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
<div style="width: 14px; height: 14px; overflow: hidden; cursor: pointer">
|
||||
<svg
|
||||
v-if="checked === 1"
|
||||
@ -58,7 +61,7 @@
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-3 text-gray-900 text-sm" v-if="showLabel">
|
||||
<div class="ml-3 text-gray-900 text-sm" v-if="showLabel && labelRight">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
</label>
|
||||
@ -71,6 +74,12 @@ export default {
|
||||
name: 'Check',
|
||||
extends: Base,
|
||||
emits: ['focus'],
|
||||
props: {
|
||||
labelRight: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
offBorderColor: 'rgba(17, 43, 66, 0.201322)',
|
||||
|
@ -1,22 +1,11 @@
|
||||
<template>
|
||||
<button
|
||||
@click="openHelpLink"
|
||||
class="
|
||||
text-gray-900
|
||||
border
|
||||
px-3
|
||||
py-2
|
||||
flex
|
||||
items-center
|
||||
mb-3
|
||||
z-10
|
||||
bg-white
|
||||
rounded-lg
|
||||
text-base
|
||||
"
|
||||
>
|
||||
<FeatherIcon class="h-6 w-6 mr-3 text-blue-400" name="help-circle" />
|
||||
<button @click="openHelpLink" class="flex items-center z-10">
|
||||
<p class="mr-1"><slot></slot></p>
|
||||
<FeatherIcon
|
||||
class="h-5 w-5 ml-3 text-blue-400"
|
||||
name="help-circle"
|
||||
v-if="icon"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
@ -27,6 +16,10 @@ import FeatherIcon from './FeatherIcon.vue';
|
||||
export default {
|
||||
props: {
|
||||
link: String,
|
||||
icon: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openHelpLink() {
|
||||
|
26
src/components/Modal.vue
Normal file
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 path from 'path';
|
||||
|
||||
function getUrlAndTokenString() {
|
||||
export function getUrlAndTokenString() {
|
||||
const inProduction = app.isPackaged;
|
||||
const empty = { url: '', telemetryUrl: '', tokenString: '' };
|
||||
let errLogCredsPath = path.join(
|
||||
process.resourcesPath,
|
||||
'../creds/err_log_creds.txt'
|
||||
'../creds/log_creds.txt'
|
||||
);
|
||||
if (!fs.existsSync(errLogCredsPath)) {
|
||||
errLogCredsPath = path.join(__dirname, '../err_log_creds.txt');
|
||||
errLogCredsPath = path.join(__dirname, '../log_creds.txt');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(errLogCredsPath)) {
|
||||
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
|
||||
return;
|
||||
return empty;
|
||||
}
|
||||
|
||||
let apiKey, apiSecret, url;
|
||||
let apiKey, apiSecret, url, telemetryUrl;
|
||||
try {
|
||||
[apiKey, apiSecret, url] = fs
|
||||
[apiKey, apiSecret, url, telemetryUrl] = fs
|
||||
.readFileSync(errLogCredsPath, 'utf-8')
|
||||
.split('\n')
|
||||
.filter((f) => f.length);
|
||||
@ -30,10 +31,14 @@ function getUrlAndTokenString() {
|
||||
console.log(`logging error using creds at: ${errLogCredsPath} failed`);
|
||||
console.log(err);
|
||||
}
|
||||
return;
|
||||
return empty;
|
||||
}
|
||||
|
||||
return { url: encodeURI(url), tokenString: `token ${apiKey}:${apiSecret}` };
|
||||
return {
|
||||
url: encodeURI(url),
|
||||
telemetryUrl: encodeURI(telemetryUrl),
|
||||
tokenString: `token ${apiKey}:${apiSecret}`,
|
||||
};
|
||||
}
|
||||
|
||||
function post(bodyJson) {
|
||||
|
@ -2,6 +2,8 @@ import { Doc, Field, FieldType, Map } from '@/types/model';
|
||||
import frappe from 'frappe';
|
||||
import { isNameAutoSet } from 'frappe/model/naming';
|
||||
import { parseCSV } from './csvParser';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { Noun, Verb } from './telemetry/types';
|
||||
|
||||
export const importable = [
|
||||
'SalesInvoice',
|
||||
@ -387,6 +389,12 @@ export class Importer {
|
||||
setLoadingStatus(true, entriesMade, docObjs.length);
|
||||
} catch (err) {
|
||||
setLoadingStatus(false, entriesMade, docObjs.length);
|
||||
|
||||
telemetry.log(Verb.Imported, this.doctype as Noun, {
|
||||
success: false,
|
||||
count: entriesMade,
|
||||
});
|
||||
|
||||
return this.handleError(doc, err as Error, status);
|
||||
}
|
||||
|
||||
@ -395,6 +403,11 @@ export class Importer {
|
||||
|
||||
setLoadingStatus(false, entriesMade, docObjs.length);
|
||||
status.success = true;
|
||||
|
||||
telemetry.log(Verb.Imported, this.doctype as Noun, {
|
||||
success: true,
|
||||
count: entriesMade,
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
ValidationError,
|
||||
} from 'frappe/common/errors';
|
||||
import BaseDocument from 'frappe/model/document';
|
||||
import config, { ConfigKeys, TelemetrySetting } from './config';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { showMessageDialog, showToast } from './utils';
|
||||
|
||||
interface ErrorLog {
|
||||
@ -17,6 +19,11 @@ interface ErrorLog {
|
||||
more?: object;
|
||||
}
|
||||
|
||||
function getCanLog(): boolean {
|
||||
const telemetrySetting = config.get(ConfigKeys.Telemetry);
|
||||
return telemetrySetting !== TelemetrySetting.dontLogAnything;
|
||||
}
|
||||
|
||||
function shouldNotStore(error: Error) {
|
||||
return [MandatoryError, ValidationError].some(
|
||||
(errorClass) => error instanceof errorClass
|
||||
@ -38,14 +45,14 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
|
||||
cb?.();
|
||||
}
|
||||
|
||||
function getToastProps(errorLogObj: ErrorLog, cb?: Function) {
|
||||
function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) {
|
||||
const props = {
|
||||
message: t`Error: ` + errorLogObj.name,
|
||||
type: 'error',
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
if (!frappe.SystemSettings?.autoReportErrors) {
|
||||
if (!canLog) {
|
||||
Object.assign(props, {
|
||||
actionText: t`Report Error`,
|
||||
action: () => {
|
||||
@ -74,6 +81,7 @@ export function handleError(
|
||||
more: object = {},
|
||||
cb?: Function
|
||||
) {
|
||||
telemetry.error(error.name);
|
||||
if (shouldLog) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -85,10 +93,11 @@ export function handleError(
|
||||
const errorLogObj = getErrorLogObject(error, more);
|
||||
|
||||
// @ts-ignore
|
||||
if (frappe.SystemSettings?.autoReportErrors) {
|
||||
const canLog = getCanLog();
|
||||
if (canLog) {
|
||||
reportError(errorLogObj, cb);
|
||||
} else {
|
||||
showToast(getToastProps(errorLogObj, cb));
|
||||
showToast(getToastProps(errorLogObj, canLog, cb));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
import config from '@/config';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import SQLiteDatabase from 'frappe/backends/sqlite';
|
||||
import fs from 'fs';
|
||||
import models from '../models';
|
||||
import regionalModelUpdates from '../models/regionalModelUpdates';
|
||||
import postStart, { setCurrencySymbols } from '../server/postStart';
|
||||
import { DB_CONN_FAILURE } from './messages';
|
||||
import { DB_CONN_FAILURE, IPC_ACTIONS } from './messages';
|
||||
import runMigrate from './migrate';
|
||||
import { callInitializeMoneyMaker, getSavePath, setLanguageMap } from './utils';
|
||||
import { getId } from './telemetry/helpers';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { callInitializeMoneyMaker, getSavePath } from './utils';
|
||||
|
||||
export async function createNewDatabase() {
|
||||
const { canceled, filePath } = await getSavePath('books', 'db');
|
||||
@ -81,6 +84,7 @@ export async function connectToLocalDatabase(filePath) {
|
||||
files = [
|
||||
{
|
||||
companyName,
|
||||
id: getId(),
|
||||
filePath,
|
||||
},
|
||||
...files.filter((file) => file.filePath !== filePath),
|
||||
@ -93,6 +97,11 @@ export async function connectToLocalDatabase(filePath) {
|
||||
|
||||
// second init with currency, normal usage
|
||||
await callInitializeMoneyMaker();
|
||||
const creds = await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS);
|
||||
telemetry.setCreds(creds?.telemetryUrl ?? '', creds?.tokenString ?? '');
|
||||
telemetry.start();
|
||||
await telemetry.setCount();
|
||||
|
||||
return { connectionSuccess: true, reason: '' };
|
||||
}
|
||||
|
||||
|
29
src/main.js
29
src/main.js
@ -4,10 +4,11 @@ import { createApp } from 'vue';
|
||||
import models from '../models';
|
||||
import App from './App';
|
||||
import FeatherIcon from './components/FeatherIcon';
|
||||
import config from './config';
|
||||
import config, { ConfigKeys } from './config';
|
||||
import { getErrorHandled, handleError } from './errorHandling';
|
||||
import { IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||
import router from './router';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { outsideClickDirective } from './ui';
|
||||
import { setLanguageMap, showToast, stringifyCircular } from './utils';
|
||||
(async () => {
|
||||
@ -16,6 +17,10 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
|
||||
await setLanguageMap(language);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.config = config;
|
||||
}
|
||||
|
||||
frappe.isServer = true;
|
||||
frappe.isElectron = true;
|
||||
frappe.initializeAndRegister(models, language);
|
||||
@ -25,7 +30,6 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
|
||||
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke);
|
||||
|
||||
window.frappe = frappe;
|
||||
window.frappe.store = {};
|
||||
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
error = error ?? new Error('triggered in window.onerror');
|
||||
@ -85,9 +89,21 @@ import { setLanguageMap, showToast, stringifyCircular } from './utils';
|
||||
console.error(err, vm, info);
|
||||
};
|
||||
|
||||
incrementOpenCount();
|
||||
app.mount('body');
|
||||
})();
|
||||
|
||||
function incrementOpenCount() {
|
||||
let openCount = config.get(ConfigKeys.OpenCount);
|
||||
if (typeof openCount !== 'number') {
|
||||
openCount = 1;
|
||||
} else {
|
||||
openCount += 1;
|
||||
}
|
||||
|
||||
config.set(ConfigKeys.OpenCount, openCount);
|
||||
}
|
||||
|
||||
function registerIpcRendererListeners() {
|
||||
ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => {
|
||||
Object.assign(window.frappe.store, message);
|
||||
@ -136,4 +152,13 @@ function registerIpcRendererListeners() {
|
||||
error.name = 'Updation Error';
|
||||
handleError(true, error);
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState !== 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, data } = telemetry.stop();
|
||||
navigator.sendBeacon(url, data);
|
||||
});
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ export const IPC_ACTIONS = {
|
||||
GET_LANGUAGE_MAP: 'get-language-map',
|
||||
CHECK_FOR_UPDATES: 'check-for-updates',
|
||||
GET_FILE: 'get-file',
|
||||
GET_CREDS: 'get-creds',
|
||||
};
|
||||
|
||||
// ipcMain.send(...)
|
||||
|
@ -320,8 +320,11 @@
|
||||
v-if="!importType"
|
||||
class="flex justify-center h-full w-full items-center mb-16"
|
||||
>
|
||||
<HowTo link="https://youtu.be/ukHAgcnVxTQ">
|
||||
{{ t`How to Use Data Import?` }}
|
||||
<HowTo
|
||||
link="https://youtu.be/ukHAgcnVxTQ"
|
||||
class="text-gray-900 rounded-lg text-base border px-3 py-2"
|
||||
>
|
||||
{{ t`How to Use Data Import` }}
|
||||
</HowTo>
|
||||
</div>
|
||||
<Loading
|
||||
|
@ -51,15 +51,17 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappe';
|
||||
import BackLink from '@/components/BackLink';
|
||||
import Button from '@/components/Button';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import SearchBar from '@/components/SearchBar';
|
||||
import Button from '@/components/Button';
|
||||
import BackLink from '@/components/BackLink';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import { IPC_ACTIONS } from '@/messages';
|
||||
import telemetry from '@/telemetry/telemetry';
|
||||
import { Verb } from '@/telemetry/types';
|
||||
import { makePDF } from '@/utils';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { IPC_ACTIONS } from '@/messages';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default {
|
||||
name: 'PrintView',
|
||||
@ -96,6 +98,7 @@ export default {
|
||||
if (!savePath) return;
|
||||
|
||||
const html = this.$refs.printContainer.innerHTML;
|
||||
telemetry.log(Verb.Exported, 'SalesInvoice', { extension: 'pdf' });
|
||||
makePDF(html, savePath);
|
||||
},
|
||||
async getSavePath() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col justify-between h-full">
|
||||
<TwoColumnForm
|
||||
v-if="doc"
|
||||
:doc="doc"
|
||||
@ -8,10 +8,27 @@
|
||||
:emit-change="true"
|
||||
@change="forwardChangeEvent"
|
||||
/>
|
||||
<div class="flex flex-row justify-between my-4">
|
||||
<LanguageSelector class="text-sm" input-class="px-4 py-1.5"/>
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<div class="flex items-center">
|
||||
<FormControl
|
||||
:df="df"
|
||||
:value="telemetry"
|
||||
@change="setValue"
|
||||
class="text-sm py-0 w-44"
|
||||
:label-right="false"
|
||||
/>
|
||||
<div class="border-r h-6 mx-2" />
|
||||
<LanguageSelector class="text-sm w-44" input-class="py-2" />
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-900 text-sm hover:bg-gray-200 rounded-md px-4 py-1.5"
|
||||
class="
|
||||
text-gray-900 text-sm
|
||||
bg-gray-100
|
||||
hover:bg-gray-200
|
||||
rounded-md
|
||||
px-4
|
||||
py-1.5
|
||||
"
|
||||
@click="checkForUpdates(true)"
|
||||
>
|
||||
Check for Updates
|
||||
@ -21,14 +38,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappe';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import { checkForUpdates } from '@/utils';
|
||||
import FormControl from '@/components/Controls/FormControl';
|
||||
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
|
||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||
import config, { ConfigKeys, telemetryOptions } from '@/config';
|
||||
import { checkForUpdates } from '@/utils';
|
||||
import frappe from 'frappe';
|
||||
|
||||
export default {
|
||||
name: 'TabSystem',
|
||||
components: {
|
||||
FormControl,
|
||||
TwoColumnForm,
|
||||
LanguageSelector,
|
||||
},
|
||||
@ -36,13 +56,27 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
doc: null,
|
||||
telemetry: '',
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.doc = frappe.SystemSettings;
|
||||
this.companyName = frappe.AccountingSettings.companyName;
|
||||
this.telemetry = config.get(ConfigKeys.Telemetry);
|
||||
},
|
||||
computed: {
|
||||
df() {
|
||||
return {
|
||||
fieldname: 'anonymizedTelemetry',
|
||||
label: this.t`Anonymized Telemetry`,
|
||||
fieldtype: 'Select',
|
||||
options: Object.keys(telemetryOptions),
|
||||
map: telemetryOptions,
|
||||
default: 'allow',
|
||||
description: this
|
||||
.t`Send anonymized usage data and error reports to help improve the product.`,
|
||||
};
|
||||
},
|
||||
fields() {
|
||||
let meta = frappe.getMeta('SystemSettings');
|
||||
return meta.getQuickEditFields();
|
||||
@ -50,6 +84,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
checkForUpdates,
|
||||
setValue(value) {
|
||||
this.telemetry = value;
|
||||
config.set(ConfigKeys.Telemetry, value);
|
||||
},
|
||||
forwardChangeEvent(...args) {
|
||||
this.$emit('change', ...args);
|
||||
},
|
||||
|
@ -5,6 +5,7 @@ import countryList from '~/fixtures/countryInfo.json';
|
||||
import importCharts from '../../../accounting/importCOA';
|
||||
import generateTaxes from '../../../models/doctype/Tax/RegionalEntries';
|
||||
import regionalModelUpdates from '../../../models/regionalModelUpdates';
|
||||
import { getId } from '../../telemetry/helpers';
|
||||
import { callInitializeMoneyMaker } from '../../utils';
|
||||
|
||||
export default async function setupCompany(setupWizardValues) {
|
||||
@ -116,6 +117,7 @@ function updateInitializationConfig(language) {
|
||||
files.forEach((file) => {
|
||||
if (file.filePath === filePath) {
|
||||
file.companyName = frappe.AccountingSettings.companyName;
|
||||
file.id = getId();
|
||||
}
|
||||
});
|
||||
config.set('files', files);
|
||||
|
@ -12,6 +12,8 @@ import QuickEditForm from '@/pages/QuickEditForm';
|
||||
import Report from '@/pages/Report';
|
||||
import Settings from '@/pages/Settings/Settings';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import telemetry from './telemetry/telemetry';
|
||||
import { NounEnum, Verb } from './telemetry/types';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -114,6 +116,28 @@ const routes = [
|
||||
|
||||
let router = createRouter({ routes, history: createWebHistory() });
|
||||
|
||||
function removeDetails(path) {
|
||||
if (!path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const match = path.match(/edit=1/);
|
||||
if (!match) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.slice(0, match.index + 4);
|
||||
}
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
const more = {
|
||||
from: removeDetails(from.fullPath),
|
||||
to: removeDetails(to.fullPath),
|
||||
};
|
||||
|
||||
telemetry.log(Verb.Navigated, NounEnum.Route, more);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.router = router;
|
||||
}
|
||||
|
124
src/telemetry/helpers.ts
Normal file
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"
|
||||
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
|
||||
|
||||
"@types/lodash@^4.14.179":
|
||||
version "4.14.179"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5"
|
||||
integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
|
Loading…
Reference in New Issue
Block a user