Skip to content

Commit

Permalink
Merge pull request #300 from HASEL-UZH/feature/295-issues-with-experi…
Browse files Browse the repository at this point in the history
…ence-sampling

Feature/295 issues with experience sampling
  • Loading branch information
casaout authored Mar 18, 2024
2 parents 23eeef6 + 46f95b7 commit 80786bc
Show file tree
Hide file tree
Showing 20 changed files with 565 additions and 108 deletions.
3 changes: 3 additions & 0 deletions documentation/INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ The following video shows the steps in action:

[![How to install PersonalAnalytics on macOS](https://markdown-videos-api.jorgenkh.no/youtube/ovfRzp3Ksgk)](https://youtu.be/ovfRzp3Ksgk)

> If you want to completely remove or reinstall PersonalAnalytics on macOS, please visit the [Troubleshooting](TROUBLESHOOTING.md#macos-reinstalling-personalanalytics-from-scratch) page.

## Using PersonalAnalytics
For the majority of the time, PersonalAnalytics is running nonintrusively in the background.
Expand All @@ -52,5 +54,6 @@ The researchers might ask you to share the collected data with them for data ana

<img width="641" alt="image" src="https://github.com/HASEL-UZH/PersonalAnalytics/assets/5212692/11c37ff9-a85b-4c45-adbf-2028cfe46a39">


### Questions
In case of questions, contact the researchers who asked you to install PersonalAnalytics. You find their email in the study description or by clicking "Get Help" in the context menu.
41 changes: 41 additions & 0 deletions documentation/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Troubleshooting PersonalAnalytics

### macOS: Reinstalling PersonalAnalytics from Scratch
> If you are experiencing issues with PersonalAnalytics on macOS and want to reinstall the app, please follow the steps below:
1. Quit PA and check that it is not running with:
```
ps aux | grep -i personal
```

2. Remove permissions for PA in System Settings:
```
open "x-apple.systempreferences:com.apple.preference.security”
```

Check for entries in “Accessibility” and “Screen & System Audio Recording”, and delete these entries using the “-“ symbol at the bottom of the list.

> Note: Checking for these permissions needs to be done *BEFORE* deleting the PA app, as permissions for deleted apps are kept, but not shown in this list if the app is deleted.
3. Delete PA and its settings

- Copy the database if you want to keep it
```
open ~/Library/Application\ Support/personal-analytics
```
- Delete PA
```
rm -rf /Applications/PersonalAnalytics.app
```
- Delete Settings
```
rm -rf ~/Library/Application\ Support/personal-analytics
```

4. Make sure PA and settings are deleted
```
ls -lah /Applications | grep -i personal
ls -lah ~/Library/Application\ Support | grep -i personal
```

5. Done - you can freshly install PersonalAnalytics now!
13 changes: 13 additions & 0 deletions src/electron/electron/config/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import logger from 'electron-log/main';
import { LogFunctions } from 'electron-log';
import { LOG_FILE_NAME, LOG_LEVEL } from './logger.config';

export const getMainLogger = (loggerName: string): LogFunctions => {
logger.transports.file.fileName = `${LOG_FILE_NAME.BACKGROUND}.log`;
logger.transports.file.maxSize = 15 * 1024 * 1024;
logger.transports.file.level = LOG_LEVEL.FILE;
logger.transports.console.level = LOG_LEVEL.CONSOLE;
return logger.scope(loggerName);
};

export default getMainLogger;
4 changes: 2 additions & 2 deletions src/electron/electron/ipc/IpcHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExperienceSamplingService } from '../main/services/ExperienceSamplingService';
import { app, ipcMain, IpcMainInvokeEvent, shell, systemPreferences } from 'electron';
import { WindowService } from '../main/services/WindowService';
import { getLogger } from '../shared/Logger';
import { getMainLogger } from '../config/Logger';
import { TypedIpcMain } from '../../src/utils/TypedIpcMain';
import Commands from '../../src/utils/Commands';
import Events from '../../src/utils/Events';
Expand All @@ -18,7 +18,7 @@ import WindowActivityDto from '../../shared/dto/WindowActivityDto';
import ExperienceSamplingDto from '../../shared/dto/ExperienceSamplingDto';
import { is } from '../main/services/utils/helpers';

const LOG = getLogger('IpcHandler');
const LOG = getMainLogger('IpcHandler');

export class IpcHandler {
private readonly actions: any;
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { release } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import log from 'electron-log/main';
import { getLogger } from '../shared/Logger';
import { getMainLogger } from '../config/Logger';
import { DatabaseService } from './services/DatabaseService';
import { SettingsService } from './services/SettingsService';
import { TrackerType } from '../enums/TrackerType.enum';
Expand Down Expand Up @@ -59,7 +59,7 @@ if (is.macOS) {

// Optional, initialize the logger for any renderer process
log.initialize();
const LOG = getLogger('Main');
const LOG = getMainLogger('Main');

app.whenReady().then(async () => {
app.setAppUserModelId('ch.ifi.hasel.personal-analytics');
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/services/AppUpdaterService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import { dialog, net } from 'electron';
import { EventEmitter } from 'events';
import updater from 'electron-updater';
import studyConfig from '../../../shared/study.config';

const { autoUpdater } = updater;

const LOG = getLogger('AutoUpdater');
const LOG = getMainLogger('AutoUpdater');

export default class AppUpdaterService extends EventEmitter {
private checkForUpdatesInterval: NodeJS.Timeout | undefined;
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/services/DataExportService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataExportType } from '../../../shared/DataExportType.enum';
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import path from 'path';
import { app } from 'electron';
import { is } from './utils/helpers';
Expand All @@ -11,7 +11,7 @@ import { Settings } from '../entities/Settings';
import { UsageDataService } from './UsageDataService';
import { UsageDataEventType } from '../../enums/UsageDataEventType.enum';

const LOG = getLogger('DataExportService');
const LOG = getMainLogger('DataExportService');

export class DataExportService {
private readonly windowActivityTrackerService: WindowActivityTrackerService =
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/services/DatabaseService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { DataSource, DataSourceOptions } from 'typeorm';
import { app } from 'electron';
import path from 'path';
import { is } from './utils/helpers';
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import { WindowActivityEntity } from '../entities/WindowActivityEntity';
import { ExperienceSamplingResponseEntity } from '../entities/ExperienceSamplingResponseEntity';
import { UserInputEntity } from '../entities/UserInputEntity';
import { Settings } from '../entities/Settings';
import { UsageDataEntity } from '../entities/UsageDataEntity';

const LOG = getLogger('DatabaseService');
const LOG = getMainLogger('DatabaseService');

export class DatabaseService {
public dataSource: DataSource;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ExperienceSamplingResponseEntity } from '../entities/ExperienceSamplingResponseEntity';
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import ExperienceSamplingDto from '../../../shared/dto/ExperienceSamplingDto';

const LOG = getLogger('ExperienceSamplingService');
const LOG = getMainLogger('ExperienceSamplingService');

export class ExperienceSamplingService {
public async createExperienceSample(
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/services/SettingsService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Settings } from '../entities/Settings';
import { generateAlphaNumericString } from './utils/helpers';
import studyConfig from '../../../shared/study.config';
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';

const LOG = getLogger('SettingsService');
const LOG = getMainLogger('SettingsService');

export class SettingsService {
public async init(): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions src/electron/electron/main/services/UsageDataService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import { UsageDataEventType } from '../../enums/UsageDataEventType.enum';
import { UsageDataEntity } from '../entities/UsageDataEntity';

const LOG = getLogger('UsageDataService');
const LOG = getMainLogger('UsageDataService');

export class UsageDataService {
public static async createNewUsageDataEvent(
Expand Down
5 changes: 3 additions & 2 deletions src/electron/electron/main/services/WindowService.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { app, BrowserWindow, Menu, nativeImage, screen, shell, Tray } from 'electron';
import { getLogger } from '../../shared/Logger';
import getMainLogger from '../../config/Logger';
import AppUpdaterService from './AppUpdaterService';
import { is } from './utils/helpers';
import path from 'path';
import MenuItemConstructorOptions = Electron.MenuItemConstructorOptions;
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import studyConfig from '../../../shared/study.config';

import { Settings } from '../entities/Settings';
import { UsageDataService } from './UsageDataService';
import { UsageDataEventType } from '../../enums/UsageDataEventType.enum';

const LOG = getLogger('WindowService');
const LOG = getMainLogger('WindowService');

export class WindowService {
private readonly appUpdaterService: AppUpdaterService;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as schedule from 'node-schedule';
import { WindowService } from '../WindowService';
import { Tracker } from './Tracker';
import { getLogger } from '../../../shared/Logger';
import getMainLogger from '../../../config/Logger';
import { Settings } from '../../entities/Settings';
import { powerMonitor } from 'electron';

const LOG = getLogger('ExperienceSamplingTracker');
const LOG = getMainLogger('ExperienceSamplingTracker');

export class ExperienceSamplingTracker implements Tracker {
private checkIfExperienceSamplingIsDueJob: schedule.Job;
Expand Down Expand Up @@ -41,15 +42,7 @@ export class ExperienceSamplingTracker implements Tracker {
LOG.info(
'Next invocation is in the past, scheduling job to fire in 30 minutes + randomization'
);
const subtractOrAdd: 1 | -1 = Math.random() < 0.5 ? -1 : 1;
const randomization =
this.intervalInMs * this.samplingRandomization * Math.random() * subtractOrAdd;
const nextInvocation = new Date(Date.now() + 30 * 60 * 1000 + randomization);

this.forcedExperienceSamplingJob = schedule.scheduleJob(nextInvocation, async () => {
await this.handleExperienceSamplingJob(nextInvocation);
});
LOG.info(`Resume, scheduled to fire at ${nextInvocation}`);
await this.scheduleNextForcedExperienceSamplingJob();
} else {
await this.startExperienceSamplingJob();
}
Expand Down Expand Up @@ -83,6 +76,39 @@ export class ExperienceSamplingTracker implements Tracker {
await settings.save();
}

private async scheduleNextForcedExperienceSamplingJob(): Promise<void> {
const subtractOrAdd: 1 | -1 = Math.random() < 0.5 ? -1 : 1;
const scheduleAfterResumeInMs = 30 * 60 * 1000;
const randomization =
scheduleAfterResumeInMs * this.samplingRandomization * Math.random() * subtractOrAdd;
const nextInvocation = new Date(Date.now() + scheduleAfterResumeInMs + randomization);

this.forcedExperienceSamplingJob = schedule.scheduleJob(nextInvocation, async () => {
const systemIdleState: 'active' | 'idle' | 'locked' | 'unknown' =
powerMonitor.getSystemIdleState(10 * 60);
LOG.debug(
`scheduleNextForcedExperienceSamplingJob(): System idle state: ${systemIdleState}, assuming idle after 10 minutes of inactivity`
);

const systemIdleTimeInSeconds: number = powerMonitor.getSystemIdleTime();
LOG.debug(
`scheduleNextForcedExperienceSamplingJob(): System idle time: ${systemIdleTimeInSeconds}`
);
if (systemIdleState !== 'active') {
LOG.info(
`scheduleNextForcedExperienceSamplingJob(): System idle time is greater than 30 minutes, not starting experience sampling job`
);
await this.scheduleNextForcedExperienceSamplingJob();
return;
}
await this.handleExperienceSamplingJob(nextInvocation);
});
LOG.info(`scheduleNextForcedExperienceSamplingJob(): scheduled to fire at ${nextInvocation}`);

await this.scheduleNextJob();
await this.startExperienceSamplingJob();
}

private getRandomNextInvocationDate(): Date {
const subtractOrAdd: 1 | -1 = Math.random() < 0.5 ? -1 : 1;
const randomization =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import * as schedule from 'node-schedule';
import { Tracker } from './Tracker';
import { TrackerConfig } from '../../../types/StudyConfig';
import { TrackerType } from '../../../enums/TrackerType.enum';
import { getLogger } from '../../../shared/Logger';
import getMainLogger from '../../../config/Logger';
import { ExperienceSamplingTracker } from './ExperienceSamplingTracker';
import { WindowService } from '../WindowService';
import studyConfig from '../../../../shared/study.config';
import { UserInputEntity } from '../../entities/UserInputEntity';
import { MoreThanOrEqual } from 'typeorm';

const LOG = getLogger('TrackerService');
const LOG = getMainLogger('TrackerService');

export class TrackerService {
private trackers: Tracker[] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ActiveWindow from 'windows-activity-tracker/dist/types/ActiveWindow';
import { WindowActivityEntity } from '../../entities/WindowActivityEntity';
import { In } from 'typeorm';
import { getLogger } from '../../../shared/Logger';
import getMainLogger from '../../../config/Logger';
import WindowActivityDto from '../../../../shared/dto/WindowActivityDto';

const LOG = getLogger('WindowActivityTrackerService');
const LOG = getMainLogger('WindowActivityTrackerService');

export class WindowActivityTrackerService {
private randomStringMap: Map<string, string> = new Map<string, string>();
Expand Down
17 changes: 0 additions & 17 deletions src/electron/electron/shared/Logger.ts

This file was deleted.

Loading

0 comments on commit 80786bc

Please sign in to comment.