From a98159841da5d60996a6fc23af9d8b102e96dcd1 Mon Sep 17 00:00:00 2001 From: Sara Hentzel Date: Thu, 21 May 2026 15:59:01 -0500 Subject: [PATCH 1/3] TT-4712 user and country analytics by year/month --- migration/r4.4/analytics.sql | 20 ++++++++++++++++++++ src/renderer/src/Sources.tsx | 4 ++++ src/renderer/src/crud/index.ts | 1 + src/renderer/src/crud/logLoginAnalytics.ts | 13 +++++++++++++ src/renderer/src/model/countryAnalytics.tsx | 13 +++++++++++++ src/renderer/src/model/index.tsx | 2 ++ src/renderer/src/model/userAnalytics.tsx | 13 +++++++++++++ src/renderer/src/schema.tsx | 19 +++++++++++++++++++ 8 files changed, 85 insertions(+) create mode 100644 migration/r4.4/analytics.sql create mode 100644 src/renderer/src/crud/logLoginAnalytics.ts create mode 100644 src/renderer/src/model/countryAnalytics.tsx create mode 100644 src/renderer/src/model/userAnalytics.tsx diff --git a/migration/r4.4/analytics.sql b/migration/r4.4/analytics.sql new file mode 100644 index 00000000..f9625899 --- /dev/null +++ b/migration/r4.4/analytics.sql @@ -0,0 +1,20 @@ +create table useranalytics ( +id serial not null primary key, +userid int not null, +year int not null, +month int not null +); +create unique index ux_useranalytics on useranalytics(userid, year, month); +grant all on useranalytics to transcriber; +grant all on useranalytics_id_seq to transcriber; + + +create table countryanalytics ( +id serial not null primary key, +country text not null, +year int not null, +month int not null +); +create unique index ux_countryanalytics on countryanalytics(country, year, month); +grant all on countryanalytics to transcriber; +grant all on countryanalytics_id_seq to transcriber; diff --git a/src/renderer/src/Sources.tsx b/src/renderer/src/Sources.tsx index fa85dac3..2de088de 100644 --- a/src/renderer/src/Sources.tsx +++ b/src/renderer/src/Sources.tsx @@ -42,6 +42,7 @@ import { updateBackTranslationType } from './crud/updateBackTranslationType'; import { updateConsultantWorkflowStep } from './crud/updateConsultantWorkflowStep'; import { serializersSettings } from './serializers/serializersFor'; import { requestedSchema } from './schema'; +import { logLoginAnalytics } from './crud/logLoginAnalytics'; import { orbitReset } from './crud/orbitReset'; type StategyError = (...args: unknown[]) => unknown; @@ -442,6 +443,9 @@ export const Sources = async ( await forceDataChanges(); console.log(`Forcing complete`); } + logLoginAnalytics(tokenState.accessToken).catch((err) => + console.log('login analytics failed', err) + ); } const user = localStorage.getItem(LocalKey.userId) as string; setUser(user); diff --git a/src/renderer/src/crud/index.ts b/src/renderer/src/crud/index.ts index 6a37ed38..d67bcb59 100644 --- a/src/renderer/src/crud/index.ts +++ b/src/renderer/src/crud/index.ts @@ -6,6 +6,7 @@ export * from './getOrgs'; export * from './groupmembership'; export * from './hasAnyRelated'; export * from './loadData'; +export * from './logLoginAnalytics'; export * from './media'; export * from './offlineError'; export * from './passage'; diff --git a/src/renderer/src/crud/logLoginAnalytics.ts b/src/renderer/src/crud/logLoginAnalytics.ts new file mode 100644 index 00000000..b1068863 --- /dev/null +++ b/src/renderer/src/crud/logLoginAnalytics.ts @@ -0,0 +1,13 @@ +import { axiosPost } from '../utils/axios'; +export async function logLoginAnalytics(token?: string | null): Promise { + axiosPost('useranalytics/track', undefined, token as string) + //.then((response) => { + // console.log( + // 'logLoginAnalytics', + // (response as { data: { data: unknown } }).data?.data + // ); + //}) + .catch((error) => { + console.error('logLoginAnalytics', error); + }); +} diff --git a/src/renderer/src/model/countryAnalytics.tsx b/src/renderer/src/model/countryAnalytics.tsx new file mode 100644 index 00000000..da845c05 --- /dev/null +++ b/src/renderer/src/model/countryAnalytics.tsx @@ -0,0 +1,13 @@ +import { InitializedRecord, UninitializedRecord } from '@orbit/records'; + +export interface CountryAnalytics extends UninitializedRecord { + attributes: { + country: string; + year: number; + month: number; + }; +} + +export type CountryAnalyticsD = CountryAnalytics & InitializedRecord; + +export default CountryAnalytics; diff --git a/src/renderer/src/model/index.tsx b/src/renderer/src/model/index.tsx index 937f2e68..f1a1f993 100644 --- a/src/renderer/src/model/index.tsx +++ b/src/renderer/src/model/index.tsx @@ -68,5 +68,7 @@ export * from './organizationBible'; export * from './paratextProject'; export * from './intellectualProperty'; export * from './vwchecksum'; +export * from './userAnalytics'; +export * from './countryAnalytics'; export * from './IExecResult'; export * from './SectionArray'; diff --git a/src/renderer/src/model/userAnalytics.tsx b/src/renderer/src/model/userAnalytics.tsx new file mode 100644 index 00000000..fd840c49 --- /dev/null +++ b/src/renderer/src/model/userAnalytics.tsx @@ -0,0 +1,13 @@ +import { InitializedRecord, UninitializedRecord } from '@orbit/records'; + +export interface UserAnalytics extends UninitializedRecord { + attributes: { + userId: number; + year: number; + month: number; + }; +} + +export type UserAnalyticsD = UserAnalytics & InitializedRecord; + +export default UserAnalytics; diff --git a/src/renderer/src/schema.tsx b/src/renderer/src/schema.tsx index f8e617ef..c4673967 100644 --- a/src/renderer/src/schema.tsx +++ b/src/renderer/src/schema.tsx @@ -1035,6 +1035,25 @@ if (requestedSchema > 9 && schemaDefinition.models) { }; schemaDefinition.version = 10; } +if (requestedSchema > 10 && schemaDefinition.models) { + schemaDefinition.models.useranalytics = { + keys: { remoteId: {} }, + attributes: { + userId: { type: 'number' }, + year: { type: 'number' }, + month: { type: 'number' }, + }, + }; + schemaDefinition.models.countryanalytics = { + keys: { remoteId: {} }, + attributes: { + country: { type: 'string' }, + year: { type: 'number' }, + month: { type: 'number' }, + }, + }; + schemaDefinition.version = 11; +} export const schema = new RecordSchema(schemaDefinition); From 72e3a06a79d6f4b68bd10fbae44418908098f51b Mon Sep 17 00:00:00 2001 From: Sara Hentzel Date: Thu, 21 May 2026 16:08:31 -0500 Subject: [PATCH 2/3] fix error handling --- src/renderer/src/Sources.tsx | 4 +--- src/renderer/src/crud/logLoginAnalytics.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/renderer/src/Sources.tsx b/src/renderer/src/Sources.tsx index 2de088de..911d903a 100644 --- a/src/renderer/src/Sources.tsx +++ b/src/renderer/src/Sources.tsx @@ -443,9 +443,7 @@ export const Sources = async ( await forceDataChanges(); console.log(`Forcing complete`); } - logLoginAnalytics(tokenState.accessToken).catch((err) => - console.log('login analytics failed', err) - ); + logLoginAnalytics(tokenState.accessToken); } const user = localStorage.getItem(LocalKey.userId) as string; setUser(user); diff --git a/src/renderer/src/crud/logLoginAnalytics.ts b/src/renderer/src/crud/logLoginAnalytics.ts index b1068863..9f02d2c1 100644 --- a/src/renderer/src/crud/logLoginAnalytics.ts +++ b/src/renderer/src/crud/logLoginAnalytics.ts @@ -1,13 +1,9 @@ import { axiosPost } from '../utils/axios'; export async function logLoginAnalytics(token?: string | null): Promise { - axiosPost('useranalytics/track', undefined, token as string) - //.then((response) => { - // console.log( - // 'logLoginAnalytics', - // (response as { data: { data: unknown } }).data?.data - // ); - //}) - .catch((error) => { - console.error('logLoginAnalytics', error); - }); + if (!token) return; + try { + await axiosPost('useranalytics/track', undefined, token); + } catch (error) { + console.error('logLoginAnalytics', error); + } } From e8fe55a19cc4fbde91c2c30e46c2857ac70343a7 Mon Sep 17 00:00:00 2001 From: Sara Hentzel Date: Fri, 22 May 2026 09:06:09 -0500 Subject: [PATCH 3/3] log errors --- src/renderer/src/Sources.tsx | 2 +- src/renderer/src/crud/logLoginAnalytics.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/Sources.tsx b/src/renderer/src/Sources.tsx index 911d903a..3e3df68d 100644 --- a/src/renderer/src/Sources.tsx +++ b/src/renderer/src/Sources.tsx @@ -443,7 +443,7 @@ export const Sources = async ( await forceDataChanges(); console.log(`Forcing complete`); } - logLoginAnalytics(tokenState.accessToken); + logLoginAnalytics(tokenState.accessToken, errorReporter); } const user = localStorage.getItem(LocalKey.userId) as string; setUser(user); diff --git a/src/renderer/src/crud/logLoginAnalytics.ts b/src/renderer/src/crud/logLoginAnalytics.ts index 9f02d2c1..2879d71a 100644 --- a/src/renderer/src/crud/logLoginAnalytics.ts +++ b/src/renderer/src/crud/logLoginAnalytics.ts @@ -1,9 +1,19 @@ +import Bugsnag from '@bugsnag/js'; import { axiosPost } from '../utils/axios'; -export async function logLoginAnalytics(token?: string | null): Promise { +import { infoMsg, logError, Severity } from '../utils'; + +export async function logLoginAnalytics( + token?: string | null, + errorReporter?: typeof Bugsnag +): Promise { if (!token) return; try { await axiosPost('useranalytics/track', undefined, token); } catch (error) { - console.error('logLoginAnalytics', error); + logError( + Severity.error, + errorReporter, + infoMsg(error as Error, 'logLoginAnalytics failed') + ); } }