diff --git a/models/types.ts b/models/types.ts new file mode 100644 index 00000000..72f59c63 --- /dev/null +++ b/models/types.ts @@ -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', +} diff --git a/src/config.js b/src/config.js deleted file mode 100644 index d95dffb6..00000000 --- a/src/config.js +++ /dev/null @@ -1,4 +0,0 @@ -import Store from 'electron-store'; - -let config = new Store(); -export default config; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..cbac5d14 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,17 @@ +import Store from 'electron-store'; + +const config = new Store(); +export default config; + +export enum ConfigKeys { + Files = 'files', + LastSelectedFilePath = 'lastSelectedFilePath', + Language = 'language', + DeviceId = 'deviceId', +} + +export interface ConfigFile { + id: string; + companyName: string; + filePath: string; +} diff --git a/src/telemetry/helpers.ts b/src/telemetry/helpers.ts new file mode 100644 index 00000000..17ad9183 --- /dev/null +++ b/src/telemetry/helpers.ts @@ -0,0 +1,120 @@ +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 { + 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; + 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; +} diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts new file mode 100644 index 00000000..080b4a56 --- /dev/null +++ b/src/telemetry/telemetry.ts @@ -0,0 +1,38 @@ +import { getCounts, getDeviceId, getInstanceId, getLocale } from './helpers'; +import { Noun, Telemetry, Verb } from './types'; + +class TelemetryManager { + #started = false; + #telemetryObject: Partial = {}; + + start() { + if (this.#started) { + return; + } + this.#telemetryObject.locale = getLocale(); + this.#telemetryObject.deviceId = getDeviceId(); + this.#telemetryObject.instanceId = getInstanceId(); + this.#telemetryObject.openTime = new Date().valueOf(); + this.#telemetryObject.timeline = []; + this.#started = true; + } + + log(verb: Verb, noun: Noun, more?: Record) { + if (!this.#started) { + this.start(); + } + + const time = new Date().valueOf(); + if (this.#telemetryObject.timeline === undefined) { + this.#telemetryObject.timeline = []; + } + + this.#telemetryObject.timeline.push({ time, verb, noun, more }); + } + async stop() { + this.#telemetryObject.counts = await getCounts(); + this.#telemetryObject.closeTime = new Date().valueOf(); + } +} + +export const telemetryManager = new TelemetryManager(); diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts new file mode 100644 index 00000000..d35a8a90 --- /dev/null +++ b/src/telemetry/types.ts @@ -0,0 +1,40 @@ +import { DoctypeName } from 'models/types'; + +export type UniqueId = string; +export type Timestamp = number; + +export interface InteractionEvent { + time: Timestamp; + verb: Verb; + noun: Noun; + more?: Record; +} + +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; + locale: Locale; +} + +export enum Verb { + Saved = 'saved', + Submitted = 'sumbitted', + Canceled = 'canceled', + Deleted = 'deleted', + Navigated = 'navigated', +} + +export enum Noun {}