Skip to content

Commit

Permalink
feat: adds country, accounts count and profile count in analytics (#2658
Browse files Browse the repository at this point in the history
)

* feat: adds country_code in analytics

* fix: getCountryCode (window not defined) issue

* refactor: move getDataFromApp to a separate function

* feat: adds profile_count and account_count in analytics

* fix: country field

* fix: try catch getCountryCode

* enhancement: add more amplitude specific properties to identify

Co-authored-by: Jean Ribeiro <[email protected]>

---------

Co-authored-by: Nicole O'Brien <[email protected]>
  • Loading branch information
jeeanribeiro and nicole-obrien authored Jun 26, 2024
1 parent b1edadb commit c5d56d3
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { ClassicLevel } from 'classic-level'
import fs from 'fs'
import path from 'path'
import { app, ipcMain } from 'electron'
import { KeyValue } from '@ui/types'
import { ThirdPartyAppName } from '@auxiliary/third-party/enums/third-party-app-name.enum'
import { getDataFromApp } from '../utils/storage.utils'

interface IThirdPartyAppManager {
getDataFromApp(appName: string): Promise<Record<string, unknown> | undefined>
copyProfileDirectory(appName: string, profileId: string): void
}

Expand All @@ -22,7 +20,10 @@ export default class ThirdPartyAppManager implements IThirdPartyAppManager {

this.removeHandlers()
ipcMain.handle('get-third-party-apps', () => this.getThirdPartyApps())
ipcMain.handle('get-data-from-third-party-app', async (_e, name) => await this.getDataFromApp(name))
ipcMain.handle(
'get-data-from-third-party-app',
async (_e, name) => await getDataFromApp(name, this.userDataPath)
)
ipcMain.handle('copy-third-party-profile', (_e, name, profileId) => this.copyProfileDirectory(name, profileId))
}

Expand All @@ -34,35 +35,6 @@ export default class ThirdPartyAppManager implements IThirdPartyAppManager {
return apps
}

public async getDataFromApp(appName: string): Promise<Record<number, KeyValue<string>> | undefined> {
const levelDBPath = path.resolve(this.userDataPath, '..', appName, 'Local Storage/leveldb')
// check if the path exists
if (!fs.existsSync(levelDBPath)) {
return
}

const data: Record<string, KeyValue<string>> = {}

try {
const db = new ClassicLevel(levelDBPath)
let i = 0
for await (const [key, value] of db.iterator()) {
data[i] = { key: key.toString(), value: value.substring(1) }
i++
}
await db.close()
return data
} catch (err) {
// https://github.com/Level/abstract-level#errors
const _err = err as Error & { code?: string }
if (_err?.code) {
throw new Error(_err.code)
} else {
console.error(err)
}
}
}

public copyProfileDirectory(appName: string, profileId: string): void {
const pathToCopy = path.resolve(this.userDataPath, '..', appName, '__storage__', profileId)
const destinationPath = path.resolve(this.userDataPath, '__storage__', profileId)
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/lib/electron/processes/main.process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { AboutWindow } from '../windows/about.window'

export let appIsReady = false

initialiseAnalytics()
void initialiseAnalytics()
initialiseDeepLinks()

/*
Expand Down
73 changes: 69 additions & 4 deletions packages/desktop/lib/electron/utils/analytics.utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { app, ipcMain } from 'electron'
import os from 'os'
import { Identify, identify, init, track } from '@amplitude/analytics-node'
import { IpApi } from '@auxiliary/country/api'

import features from '@features/features'

import { getPlatformVersion } from './diagnostics.utils'
import { getMachineId } from './os.utils'
import { getDataFromApp } from './storage.utils'
import { IPersistedProfile } from '@core/profile'

export function initialiseAnalytics(): void {
export async function initialiseAnalytics(): Promise<void> {
if (features.analytics.enabled && process.env.AMPLITUDE_API_KEY) {
// Initialise Amplitude with API key
init(process.env.AMPLITUDE_API_KEY, { logLevel: 0 })

setInitialIdentify()
await setInitialIdentify()
// Register event handlers
ipcMain.handle('track-event', (_e, event, properties) => handleTrackEvent(event, properties))
} else {
Expand All @@ -27,7 +30,53 @@ function handleTrackEvent(event: string, properties: Record<string, unknown>): v
track(event, properties, { device_id: getMachineId() })
}

function setInitialIdentify(): void {
async function getLocation(): Promise<
| {
country: string
region: string
city: string
}
| undefined
> {
try {
const api = new IpApi()
const location = await api.getLocation()
return location
} catch (err) {
console.error(err)
return undefined
}
}

async function getProfilesAndAccountsCount(): Promise<{ profiles: number; accounts: number }> {
const userDataPath = app.getPath('userData')
const appName = app.getName()

let profiles: IPersistedProfile[] = []
try {
const data = await getDataFromApp(appName, userDataPath)
if (!data) {
return { profiles: 0, accounts: 0 }
}
const separator = String.fromCharCode(1)
Object.values(data).forEach(({ key, value }) => {
if (key.split(separator)[1] === 'profiles') {
profiles = JSON.parse(value)
return
}
})
} catch (err) {
console.error(err)
return { profiles: 0, accounts: 0 }
}

const profilesCount = profiles.length
const accountsCount = profiles.reduce((acc, profile) => acc + Object.keys(profile.accountPersistedData).length, 0)

return { profiles: profilesCount, accounts: accountsCount }
}

async function setInitialIdentify(): Promise<void> {
const identifyObj = new Identify()

// Application Information
Expand All @@ -39,5 +88,21 @@ function setInitialIdentify(): void {
identifyObj.setOnce('platform_architecture', os.arch())
identifyObj.set('platform_version', getPlatformVersion())

identify(identifyObj, { device_id: getMachineId() })
// User Information
const { profiles, accounts } = await getProfilesAndAccountsCount()
identifyObj.set('profile_count', profiles)
identifyObj.set('account_count', accounts)

const location = await getLocation()

const app_version = app.isPackaged ? app.getVersion() : 'dev'

identify(identifyObj, {
device_id: getMachineId(),
app_version,
platform: os.platform(),
os_name: os.type(),
os_version: getPlatformVersion(),
...location,
})
}
36 changes: 36 additions & 0 deletions packages/desktop/lib/electron/utils/storage.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { KeyValue } from '@ui/types'
import { ClassicLevel } from 'classic-level'
import path from 'path'
import fs from 'fs'

export async function getDataFromApp(
appName: string,
userDataPath: string
): Promise<Record<number, KeyValue<string>> | undefined> {
const levelDBPath = path.resolve(userDataPath, '..', appName, 'Local Storage/leveldb')
// check if the path exists
if (!fs.existsSync(levelDBPath)) {
return
}

const data: Record<string, KeyValue<string>> = {}

try {
const db = new ClassicLevel(levelDBPath)
let i = 0
for await (const [key, value] of db.iterator()) {
data[i] = { key: key.toString(), value: value.substring(1) }
i++
}
await db.close()
return data
} catch (err) {
// https://github.com/Level/abstract-level#errors
const _err = err as Error & { code?: string }
if (_err?.code) {
throw new Error(_err.code)
} else {
console.error(err)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CountryApi } from '../api'
import { IpApi } from '../api'
import { updateCountryCode } from '../stores'

export async function getAndUpdateCountryCode(): Promise<void> {
const api = new CountryApi()
const api = new IpApi()
const countryCode = await api.getCountryCode()
updateCountryCode(countryCode)
}
24 changes: 22 additions & 2 deletions packages/shared/src/lib/auxiliary/country/api/country.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseApi } from '@core/utils'
import { BaseApi } from '@core/utils/api'

interface IIpApiResponse {
ip: string
Expand Down Expand Up @@ -30,11 +30,31 @@ interface IIpApiResponse {
org: string
}

export class CountryApi extends BaseApi {
export class IpApi extends BaseApi {
constructor() {
super('https://ipapi.co')
}

async getLocation(): Promise<
| {
country: string
region: string
city: string
}
| undefined
> {
const ipData = await this.get<IIpApiResponse>('json')
if (ipData === undefined) {
return undefined
}

return {
country: ipData?.country,
region: ipData?.region,
city: ipData?.city,
}
}

async getCountryCode(): Promise<string | undefined> {
const ipData = await this.get<IIpApiResponse>('json')
return ipData?.country_code
Expand Down
8 changes: 6 additions & 2 deletions packages/shared/src/lib/core/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { localize } from '@core/i18n'
import { QueryParameters } from './types'
import { buildUrl } from './url'

Expand Down Expand Up @@ -55,7 +54,12 @@ export class BaseApi {
query: queryParameters,
})
if (!url) {
throw localize('error.global.invalidUrl')
if (window) {
const { localize } = await import('@core/i18n')
throw localize('error.global.invalidUrl')
} else {
throw new Error('Invalid URL')
}
}

const response = await fetch(url.href, requestInit)
Expand Down

0 comments on commit c5d56d3

Please sign in to comment.