diff --git a/README.md b/README.md index af5fcca5..ffe12f34 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Anyone is welcome to contribute to PersonalAnalytics by extending it with new tr ## 🧑‍💻 Installation & Usage as a User Anyone may install PersonalAnalytics on their Windows or macOS device to non-intrusively collect computer interaction data, and analyze their activity, time spent and work habits for themselves. In the future, once we'll re-introduce the Retrospection (i.e. visualizations of the collected and self-reported data), it will be much easier to gain insights again. -To install and use PersonalAnalytics, [consider this guide](./documentation/INSTALLATION.md). +Learn more about how to [install and use PersonalAnalytics](./documentation/INSTALLATION.md). ## 👩‍🔬 Customization & Usage as a Researcher This project was created by and for researchers who want to ask study participants to run PersonalAnalytics on their device to non-intrusively collect **computer interaction data** in a privacy-protected way. As often times, having access to only automatically collected data is often not sufficient, PersonalAnalytics also offers an **experience sampling component**, which allows researchers to ask users to reflect and self-report on one or several questions (e.g. Have I been productive? Am I stressed right now?) at customizable times and using Likert-scales. As all collected data is only stored locally on participants' computers, there is an **export component**, guiding the participant through sharing and potentially obfuscating the captured data, before sharing it with the researchers through their data transfer service of choice. Most settings are configurable in the [study-config]([url](https://github.com/HASEL-UZH/PersonalAnalytics/blob/feature/electron/src/electron/shared/study.config.ts)), everything else can be customized in code. @@ -21,6 +21,7 @@ Learn more about how to use [PersonalAnalytics for your research project](./docu ## 📖 Further Information - [Installation & Usage for End Users](./documentation/INSTALLATION.md) - [Customization & Usage for Researchers](./documentation/RESEARCH.md) +- [Reporting an issue or bug](https://github.com/HASEL-UZH/PersonalAnalytics/issues) - [Data Collection & Privacy Policy](./documentation/PRIVACY.md) - [Contributions](./documentation/RESEARCH.md#contributions-guide) - [Information on the old PersonalAnalytics](./documentation/LEGACY.md) @@ -37,11 +38,23 @@ In 2024, we've revived the project in creating a multi-platform app using TypeSc This work is carried by the following main contributors: - [Dr. André Meyer](https://www.andre-meyer.ch) (University of Zurich, main contributor to the project) - [Prof. Dr. Thomas Fritz](http://www.ifi.uzh.ch/en/seal/people/fritz.html) (University of Zurich) -- [Chris Satterfield](https://github.com/csatterfield) (contributor to MacOS version) +- [Sebastian Richner](https://github.com/SRichner) (contributor to new version) - [Roy Rutishauser](https://github.com/royru) (contributor to MacOS-legacy version) +- [Chris Satterfield](https://github.com/csatterfield) (contributor to MacOS-legacy version) - [Jan Pilzer](https://github.com/hirse) (contributor to Windows-legacy version) -- [Sebastian Richner](https://github.com/SRichner) (contributor to new version) +- [Alexander Lill](https://github.com/alexanderlill) (tester) +- [Isabelle Cuber](https://github.com/isicu) (tester) - Dr. Manuela Züger (prev. University of Zurich, contributor to Windows-legacy version) - Dr. Sebastian Müller (prev. University of Zurich, contributor to Windows-legacy version) - [Dr. Tom Zimmermann](https://www.microsoft.com/en-us/research/people/tzimmer/) (Microsoft Research) - [Prof. Dr. Gail C. Murphy](https://blogs.ubc.ca/gailcmurphy/) (University of British Columbia) + + +## 📨 Contact +- You may contact André Meyer (ameyer@ifi.uzh.ch) in case of questions on the project. +- Do not attempt contact in case of questions on a specific study in which you are participating. If you encounter technical issues, create an [issue](https://github.com/HASEL-UZH/PersonalAnalytics/issues), so that the community may offer help. + + +## ↪️ Dependencies +This project uses [PA.WindowsActivityTracker](https://github.com/HASEL-UZH/PA.WindowsActivityTracker) and [PA.UserInputTracker](https://github.com/HASEL-UZH/PA.UserInputTracker/) as its main data trackers. +For the full list of dependencies, consider [package.json](./src/electron/package.json). diff --git a/documentation/ARCHITECTURE.md b/documentation/ARCHITECTURE.md deleted file mode 100644 index ce938493..00000000 --- a/documentation/ARCHITECTURE.md +++ /dev/null @@ -1,38 +0,0 @@ -# Architecture Overview `Work in Progress` - -This document provides a brief overview over the architecture of PersonalAnalytics. It is still work in progress. - -## Supported Platforms -At the moment, the following platforms are supported and PersonalAnalytics is extensively tested on them: -- Windows 8.1 and Windows 10 (Windows 7 is no longer supported) [Link](https://github.com/HASE-UZH/PersonalAnalytics/tree/dev-am/src/windows/) -- macOS 10.12 or newer [Link](https://github.com/HASE-UZH/PersonalAnalytics/tree/dev-am/src/macOS/) -In the future, the aim is to also provide similar apps for mobile (Android and iOS) and Linux. Contributions are welcome. - -## Basic Concepts -- PersonalAnalytics features a number of Data Trackers, each responsible for tracking, aggregating and storing a certain type of data (e.g. user input). -- PersonalAnalytics was built with extensibility in mind, meaning that it is very easy to add a new Data Tracker to PersonalAnalytics and dynamically enable/disable them, depending on the usage scenarios (e.g. private usage or research project). - -## Data Trackers -`TODO: complete description and table` -- Data Trackers can be enabled or disabled -- Data Trackers implement the `ITracker` interface -- Visualizations are only provided in the retrospection for enabled trackers - -| Data Tracker | Collected Data | Database Table Name | OS Support | Maturity | -|------------------------|----------------|---------------------|------------|----------| -| WindowsActivityTracker | for each application used, the time, duration, process name and window title is stored | windows_activity | Windows, macOS | frequently used | -| UserInputTracker | mouse movement, clicks, scrolls; keyboard strokes (not actual keys, only type (any, navigate, delete) | user_input | Windows, macOS | frequently used | -| MsOfficeTracker | Email info (number of emails in inbox, sent, received, unread), meeting info (time, subject, duration, number of attendees) | emails, meetings | Windows | frequently used | -| UserEfficiencyTracker | participants' self-reports in interval pop-up (e.g. perceived productivity) | user_efficiency_survey, user_efficiency_survey_day | Windows, macOS | frequently used | -| FitbitTracker | synced data, such as heart rate | fitbit | Windows | not actively tested for 1-2 years | -| PolarTracker | synced data, such as heart rate | polar | Windows | not actively tested for 1-2 years | - -`Note:` The project is currently not maintained super actively. However, the most important data trackers were ported to run on both `Windows` and `macOS` using `Typescript` and some OS-native code. They are OSS, actively used and maintained here: -- [WindowsActivityTracker](https://github.com/HASEL-UZH/PA.WindowsActivityTracker/tree/main/typescript) that allows to log the timestamp, app name and window title of the currently active window, in addition to an automated catgorization into the `Activity` (method described in this [publication](https://andre-meyer.ch/TSE20). -- [UserInputTracker](https://github.com/HASEL-UZH/PA.UserInputTracker/tree/main/typescript) that allows to log moue movement, clicks, scrolls and keystrokes with timestamps (not actual keys, only the type (any, navigate, delete) for privacy reasons) - -## Retrospection -`TODO: describe basics and functionality of retrospection` - - -![Retrospection Screenshot](./images/retrospection_screenshot.png?raw=true) diff --git a/documentation/INSTALLATION.md b/documentation/INSTALLATION.md index 98b6856d..3cd0d2cc 100644 --- a/documentation/INSTALLATION.md +++ b/documentation/INSTALLATION.md @@ -2,13 +2,13 @@ Anyone may install PersonalAnalytics on their Windows or macOS device to non-intrusively collect computer interaction data, and analyze their activity, time spent and work habits for themselves. In the future, once we'll re-introduce the Retrospection (i.e. visualizations of the collected and self-reported data), it will be much easier to gain insights again. ## How to install PersonalAnalytics on Windows -1. Visit https://hasel.dev/pa +1. Visit https://hasel.dev/pa (or use the link provided to you by the researchers) 2. Select the Windows version (exe-file) 3. Wait for the download to complete 4. Allow the install-file (exe) to be downloaded in case your browser blocks it 5. Double click the downloaded file and follow the installer -6. Select "Run PersonalAnalytics" -7. Follow the Onboarding wizard that explains the study, collected data and how to use PersonalAnalytics +6. Select "Run PersonalAnalytics" before closing the installer +7. Follow the Onboarding-wizard that explains the study, collected data and how to use PersonalAnalytics 8. Access PersonalAnalytics anytime through the context-menu in the taskbar icon The following video shows the steps in action: @@ -17,8 +17,20 @@ The following video shows the steps in action: ## How to install PersonalAnalytics on macOS +1. Visit https://hasel.dev/pa (or use the link provided to you by the researchers) +2. Select the macOS version (dmg-file) +3. Wait for the download to complete +4. Open the downloaded instller and drag the PersonalAnalytics-app to your Applications folder +5. (optional) Click "Open" in case a warning is shown that the app was downloaded from the internet +6. After the installation completes, follow the Onboarding-wizard, which explains the study, collected data and how to use PersonalAnalytics +7. On the second page, grant PersonalAnalytics the permissions it requires to function corectly +8. Note that you might need to manually quit and restart PersonalAnalytics after giving permission +9. Access PersonalAnalytics anytime through the context-menu in the menubar icon + +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) - ## Using PersonalAnalytics For the majority of the time, PersonalAnalytics is running nonintrusively in the background. diff --git a/documentation/PRIVACY.md b/documentation/PRIVACY.md index 34f7d0f8..17458c6c 100644 --- a/documentation/PRIVACY.md +++ b/documentation/PRIVACY.md @@ -1,11 +1,24 @@ # Data Storage & Confidentiality -PersonalAnalytics is a Windows and macOS application designed to be installed on knowledge workers computers to non-intrusively collect data about their work and work habits, including application usage, user input, websites visited, files worked on, emails and meetings, amongst others. +## Goal & Overview +PersonalAnalytics is a Windows and macOS application designed to be installed on knowledge workers' computers to non-intrusively collect computer interaction data, including user input and application usage data, as well as self-reporte data using an experience sampling component. The idea is that researchers can deploy PersonalAnalytics with study participants to allow them collect data during baseline and intervention phases and help them to answer their research questions. When creating a release of PersonalAnalytics, researchers can configure which data is collected. All data is stored **only locally** and **never automatically shared with the researchers without a user's explicit consent**. -All data (tracked and self-reported data) is **stored ONLY locally on the knowledge worker's computer**. It is NOT uploaded to any servers. This also includes stored credentials, e.g. of the Microsoft Office 365 service or Fitbit service. When running, the context menu of the app has a menu item "Open Collected Data" which opens the directory where all the collected data is stored. In most cases, the directory contains a log file (text-file), a database file (pa.dat) and the hashed token for accessing the Microsoft Office 365 service. +## Overview over Collected Data +Currently, there are three data trackers collecting data and storing it locally on user's computers: +- **User Input Tracker**: User input data stems from mouse (number of clicks, pixels moved and pixels scrolled) and keyboard (number of keystrokes) and is aggregated per interval (e.g. once a minute). Note that the actual keys pressed are _not_ stored (no keylogging!) +- **Windows Activity Tracker**: The application usage data includes an entry for each time a user switches from one app, website or file to another, storing the time of switch, app name and window title. The application usage is then automatically categorized using [research-based heuristics]([url](https://www.zora.uzh.ch/id/eprint/136503/1/productiveWorkday_TSE17.pdf)) ([source]([url](https://github.com/HASEL-UZH/PA.WindowsActivityTracker/tree/main/typescript/src/mappings))). +- **Experience Sampling Tracker**: Researchers can also define one or multiple questions that are shown to users at random intervals and ask to provide a rating to a question, such as on their perceived productivity, well-being, or stress levels. Only the question, rating (e.g. 5/7) and timestamp are stored. -To manually review, modify and delete the collected data, the user needs to open the `pa.dat` file using [SQLite Browser](https://sqlitebrowser.org/). +## Accessing, Modifying or Deleting the Data +As mentioned above, all data is stored locally only on participant's machines. Users can access it, by clicking "Open Collected Data" in the taskbar icon (on Windows) or menubar (on macOS) and opening the file `database.sqlite` in a sqlite-compatible database viewer (such as [DB Browser for SQLite](https://sqlitebrowser.org/). -In the settings, the user can enable or disable the different data trackers. In case a user is doing sensitive work, she can temporarily pause PersonalAnalytics (via the context menu) or shut it down completely. +Should a user want to modify and/or delete their data, they can do so directly in the sqlite-file. No other copies of the data exists, unless the user made them. -During research studies, the researchers might ask users (i.e. study participants) to share the collected data with the researchers. They might or might not offer to obfuscate the collected data beforehand. This step depends entirely on the researchers and the owners and contributors of this project have **no control and/or responsibility** over this step. They encourage any researchers who use PersonalAnalytics for their research projects, to (1) be very transparent with what data is collected, (2) what the collected data will be used for (once the participants shares it with the researchers), (3) to only collect the absolute minimum of data, (4) to provide participants with a consent form to allow them to give informed consent on the collected data and its usage, and (5) to allow participants to require the deletion of their data. +## Sharing Collected Data +In case users are running PersonalAnalytics during a scientific study, the researchers might ask the users (or in this context, participants) to share their data with the reseachers. To that purpose, we recommend using the built-in data obfuscation and export feature, which allows users to understand what the data will be used for as part of the research project, review the collected data and decide which data they want to share and/or obfuscate. Afterwards, an encrypted and password-protected export-file is created which can be shared with the researchers per their instructions. The data export tool can be accessed by clicking "Export Data" in the taskbar icon (on Windows) or menubar (on macOS). + +## Note on Using PersonalAnalytics +Note that the creators of PersonalAnalytics can in no way be held liable against use, misuse or problems that arise from using the app. The app was developed as a public, open-source application that can be freely used and extended (with [correct attribution](https://github.com/HASEL-UZH/PersonalAnalytics/blob/main/documentation/RESEARCH.md). The researchers are responsible for informing users (or participants) of the usage of PersonalAnalytics, collected data and usage of any data that is shared with researchers, as well as data privacy and data security. + +## Questions and Support +You may contact André Meyer (ameyer@ifi.uzh.ch) in case of questions on the project. Do not contact in case of questions on a specific study in which you are participating. If you encounter technical issues, create an [issue]([url](https://github.com/HASEL-UZH/PersonalAnalytics/issues)https://github.com/HASEL-UZH/PersonalAnalytics/issues), so that the community may offer help. diff --git a/documentation/RESEARCH.md b/documentation/RESEARCH.md index 49642c5e..dd2431e4 100644 --- a/documentation/RESEARCH.md +++ b/documentation/RESEARCH.md @@ -3,25 +3,135 @@ This project was created by and for researchers who want to ask study participants to run PersonalAnalytics on their device to non-intrusively collect computer interaction data in a privacy-protected way. As often times, having access to only automatically collected data is often not sufficient, PersonalAnalytics also offers an experience sampling component, which allows researchers to ask users to reflect and self-report on one or several questions (e.g. Have I been productive? Am I stressed right now?) at customizable times and using Likert-scales. As all collected data is only stored locally on participants' computers, there is an export component, guiding the participant through sharing and potentially obfuscating the captured data, before sharing it with the researchers through their data transfer service of choice. Most settings are configurable in the study-config, everything else can be customized in code. -# Customizing PersonalAnalytics +## Customizing PersonalAnalytics To customize PersonalAnalytics for your research study, please consider the following steps: - - 1. Fork the project to work in your own repository. -2. Update the `study.config.ts`-[file](../src/electron/shared/study.config.ts) with your custom study-related settings. Hereby, you can add your custom study name, study title, privacy policy, export upload url as well as contact data. In addition, you can customize which computer interaction tracker isrunning, and if you want to prompt the user to self-report on one or several questions in the experience sampling component. +2. Update the [study configuration-file](../src/electron/shared/study.config.ts) (`study.config.ts`) with your custom study-related settings (see details below). Hereby, you can add your custom study name, study title, privacy policy, export upload url as well as contact data. In addition, you can customize which computer interaction tracker isrunning, and if you want to prompt the user to self-report on one or several questions in the experience sampling component. 3. (optional) If you require further customizations, you can create them in the code (see [Contributions Guide](#contributions-guide)). -4. Use Github Actions (see [build.yml](https://github.com/HASEL-UZH/PersonalAnalytics/blob/feature/electron/.github/workflows/build.yml)) to build and deploy PersonalAnalytics and allow your participants to use it. Using the method, PersonalAnalytics can automatically update your participants' installations with new releases +4. Use GitHub Actions (see [build.yml](https://github.com/HASEL-UZH/PersonalAnalytics/blob/feature/electron/.github/workflows/build.yml)) to build and deploy PersonalAnalytics and allow your participants to use it. Using the method, PersonalAnalytics can automatically update your participants' installations with new releases + +When creating new releases, update the package.json file with the new version number and commit and push it. + +### Required GitHub Secrets +To use GitHub Actions to build and create PersonalAnalytics releases, you need to set the following secrets in your repository: +- `GH_TOKEN` (a GitHub token with the `repo` scope) +- `APPLE_ID` (your Apple ID) +- `APPLE_APP_SPECIFIC_PASSWORD` (an app-specific password for your Apple ID) +- `APPLE_TEAM_ID` (your Apple Team ID) +- `CSC_LINK` +- `CSC_KEY_PASSWORD` + +### Required Changes in `electron-builder.json5` +These changes are required to automatically publish the built artifacts to GitHub releases. You need to replace the `owner` and `repo` with your GitHub username and repository name. +You can find more information on electron-builder here: https://www.electron.build/ and for the `electron-builder.json5` file here: https://www.electron.build/configuration/configuration + +```json5 +{ + publish: { + provider: "github", + owner: "YOUR_GITHUB_USER_OR_ORGANIZATION", + repo: "YOUR_REPOSITORY_NAME", + } +} +``` +### General Configuration of PersonalAnalytics (edit in `study.config.ts`) +| Parameter | Description | Change Required | Default Value | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|---------------| +| `name` | The name of the study. It is shown in various places of PersonalAnalytics, such as the the about page, the experience sampling and when exporting the study data. | ✅ | | +| `shortDescription` | A short description of the study. It is shown during the onboarding process, in the about page, and when exporting the study data. It should describe the study goal and summarize the collected data and how the data is analyzed. | ✅ | | +| `infoUrl` | A link to a website (starting with `https://`) to provide additional details about the study. It is shown during the onboarding process, in the about page and when exporting the study data. | ✅ | | +| `privacyPolicyUrl` | A link to a website (starting with `https://`) that describes the privacy policy of the study. It is shown during the onboarding process, in the about page and when exporting the study data. | ✅ | | +| `uploadUrl` | A link to a website (starting with `https://`) that offers file-uploads for the participants to share their study data (e.g. SharePoint, Dropbox, Dropfiles website). It is shown when exporting the study data. | ✅ | | +| `contactName` | The name of the Principal Investigator (PI) that participants can contact. It is shown during the onboarding process, in the about page, when exporting the study data, in case of errors and in the application's menu when requesting help or reporting an issue. | ✅ | | +| `contactEmail` | An email address that participants can use to contact the researchers. It is shown during the onboarding process, in the about page, when exporting the study data, in case of errors and in the application's menu when requesting help or reporting an issue. | ✅ | | +| `subjectIdLength` | The length of the subject ID that is automatically generated for each participant when PersonalAnalytics is installed (e.g. `8JE7DA`). | | `6` | +| `dataExportEnabled` | Whether the participant should be able to export their data. If enabled, participants can export their study data through the context menu. | | `true` | -# Contributions Guide +### Tracker Configuration (edit `trackers` in `study.config.ts`) +#### WindowsActivityTracker +| Parameter | Description | Change Required | Default Value | +|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|---------------| +| `enabled` | Whether the WindowsActivityTracker is enabled. If enabled, the WindowsActivityTracker will collect data about the participant's application usage, including app names, window titles, visited URLs etc. | | `true` | +| `intervalInMs` | The interval in milliseconds at which the WindowsActivityTracker should collect data. We recommend to keep the default. | | `1000` | +| `trackUrls` | Whether the WindowsActivityTracker should track the URLs of the websites that the participant visits. This feature only works on macOS. When enabled, participants need to enable the accessibility permission through the system settings, and will automatically be prompted to do so when running the application for the first time. | | `false` | +| `trackWindowTitles` | Whether the WindowsActivityTracker should track the titles of the windows that the participant uses. On macOS and when enabled, participants need to enable the screen recording permission through the system settings, and will automatically be prompted to do so when running the application for the first time. | | `true` | + +#### UserInputTracker +| Parameter | Description | Change Required | Default Value | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|---------------| +| `enabled` | Whether the UserInputTracker is enabled. If enabled, the UserInputTracker will collect data about the participant's keyboard and mouse input (number of keystrokes, number of clicks, pixels moved and pixels scrolled in the defined interval). | | `true` | +| `intervalInMs` | The interval in milliseconds at which the UserInputTracker collects and stores the aggregated data. | | `10000` | + +#### ExperienceSamplingTracker +| Parameter | Description | Change Required | Default Value | +|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|---------------------------| +| `enabled` | Whether the ExperienceSamplingTracker is enabled. If enabled, the ExperienceSamplingTracker will prompt the participant to self-report on one or several questions at customizable times (see configuration below). | | `true` | +| `intervalInMs` | The interval in milliseconds at which the ExperienceSamplingTracker should prompt the participant to self-report. | | `1000*60*60*3` (3 hours) | +| `samplingRandomization` | Whether the ExperienceSamplingTracker should include a randomization for the time at which the participant is prompted to self-report. A value between 0 and 1. If enabled (value bigger than 0), the ExperienceSamplingTracker will randomly calculate a value that is between `intervalInMs` plus/minus `intervalInMs * samplingRanomization`. If the `intervalInMs` is set to `1000 * 60 * 60 * 3` (3 hours) and the `samplingRanomization` to `0.1`, the prompt may be shown in 162 - 198 minutes. | | `0.1` | +| `scale` | The number of items of the Likert-scale. Per the definition, it should be an odd number, ideally between 3-9. It applies to all questions. | | `7` | +| `questions` | An array of questions that the participant should self-reflect on. You can define one or multiple questions within the array (e.g., `['I am more productive in my current work session compared to the last one.']`). If the array consists of multiple questions, the question will be randomly selected. | ✅ | | +| `responseOptions` | An array of arrays with labels for the Likert-scale question. This can be either two labels that will be displayed on the left and right (e.g., `['strongly disagree', 'strongly agree']`) or three labels (e.g., `['strongly disagree', 'neutral', 'strongly agree']`). The same order as defined for the `questions` applies. | ✅ | | + + +## Contributions Guide Anyone is welcome to contribute to PersonalAnalytics by extending it with new trackers or improving existing ones. -This quick guide helps you to set-up your development environment: - +1. Fork the project to work in your own repository. +2. Create a new branch for your changes. +3. Make your changes and commit them to your branch. +4. Push your branch to your fork. +5. Create a pull request from your branch to the `main` branch of the main repository. +6. Wait for the maintainers to review your pull request. +7. If your pull request is approved, it will be merged into the main repository. +8. If your pull request is not approved, you can make further changes and push them to your branch. The pull request will be updated automatically. +### Install the dependencies +After cloning this repository using your favorite git client, you need to install the dependencies. +Make sure you use node version >=20. You can install the dependencies by running the following command in the root directory of the project: +```bash +cd src/electron +npm install +``` +This will install all the dependencies required to build and run PersonalAnalytics. This will also call the `postinstall` script, which will make sure that the native dependencies are built for your platform. -# Research that used PersonalAnalytics +### Starting the application for development +To start the application for development, you can run the following command in `src/electron`: +```bash +npm run dev +``` + +### Building the application +To build the application, you can run the following command in `src/electron`: +```bash +npm run build +``` +This will build the application for your platform and architecture. The built application will be located in the `release` directory. + + +You can also run the following command to build the application for Windows: +```bash +npm run build:win +``` +or for macOS (only on macOS): +```bash +npm run build:mac +``` + +### Testing PersonalAnalytics +PersonalAnalytics was tested on `Windows 11` and `macOS 14`. It might work on older versions as well. + +## Referencing PersonalAnalytics +When leveraging PersonalAnalytics for your work or research, please cite it appropriately, by refering to the main publication as well as this repository. + +Citing the paper: +`Meyer, A. N., Murphy, G. C., Zimmermann, T., & Fritz, T. (2017). Design recommendations for self-monitoring in the workplace: Studies in software development. Proceedings of the ACM on Human-Computer Interaction, 1(CSCW), 1-24. https://doi.org/10.1145/3134714` + +Citing the repository: +`https://github.com/HASEL-UZH/PersonalAnalytics` + +## Research that used PersonalAnalytics PersonalAnalytics-legacy was used in the following peer-reviewed research projects (and other non-peer reviewed projects too, such as master and bachelor theses): - [CHI'20](https://andre-meyer.ch/CHI20) Supporting Software Developers’ Focused Work on Window-Based Desktops. Jan Pilzer, Raphael Rosenast. André Meyer. Elaine Huang. Thomas Fritz. - [TSE'20](https://andre-meyer.ch/TSE20) Detecting Developers’ Task Switches and Types. André Meyer, Chris Satterfield, Manuela Züger, Katja Kevic, Gail Murphy, Thomas Zimmermann, and Thomas Fritz. @@ -29,3 +139,6 @@ PersonalAnalytics-legacy was used in the following peer-reviewed research projec - [CHI’18](http://www.zora.uzh.ch/id/eprint/151128/1/pn4597-zugerA.pdf) Sensing Interruptibility in the Office: A Field Study on the Use of Biometric and Computer Interaction Sensors. Manuela Züger, Sebastian Müller, André Meyer, Thomas Fritz. - [TSE’17](https://www.andre-meyer.ch/TSE17) The Work Life of Developers: Activities, Switches and Perceived Productivity. André Meyer, Gail Murphy, Thomas Zimmermann, Laura Barton, Thomas Fritz. - [CHI’17](https://www.andre-meyer.ch/CHI17) Reducing Interruptions at Work: A Large-Scale Field Study of FlowLight. Manuela Züger, Christopher Corley, André Meyer, Boyang Li, Thomas Fritz, David Shepherd, Vinay Augustine, Patrick Francis, Nicholas Kraft and Will Snipes. + +## Questions & Support +Please contact André Meyer (ameyer@ifi.uzh.ch) in case of questions. diff --git a/documentation/videos/PersonalAnalytics_Installation_macOS.mp4 b/documentation/videos/PersonalAnalytics_Installation_macOS.mp4 new file mode 100644 index 00000000..efa881fa Binary files /dev/null and b/documentation/videos/PersonalAnalytics_Installation_macOS.mp4 differ diff --git a/src/electron/electron-builder.json5 b/src/electron/electron-builder.json5 index ee1b51f9..a1b342d0 100644 --- a/src/electron/electron-builder.json5 +++ b/src/electron/electron-builder.json5 @@ -54,9 +54,7 @@ artifactName: '${productName}-Windows-${version}-Setup.${ext}' }, nsis: { - oneClick: false, - perMachine: false, - allowToChangeInstallationDirectory: true, + oneClick: true, deleteAppDataOnUninstall: false, differentialPackage: false }, diff --git a/src/electron/electron/enums/UsageDataEventType.enum.ts b/src/electron/electron/enums/UsageDataEventType.enum.ts new file mode 100644 index 00000000..405d9bfc --- /dev/null +++ b/src/electron/electron/enums/UsageDataEventType.enum.ts @@ -0,0 +1,15 @@ +export enum UsageDataEventType { + AppStart = 'APP_START', + AppQuit = 'APP_QUIT', + SystemLockScreen = 'SYSTEM_LOCK_SCREEN', + SystemUnlockScreen = 'SYSTEM_UNLOCK_SCREEN', + SystemSuspend = 'SYSTEM_SUSPEND', + SystemResume = 'SYSTEM_RESUME', + SystemShutdown = 'SYSTEM_SHUTDOWN', + StartExport = 'START_EXPORT', + FinishExport = 'FINISH_EXPORT', + ExperienceSamplingManuallyOpened = 'EXPERIENCE_SAMPLING_MANUALLY_OPENED', + ExperienceSamplingAutomaticallyOpened = 'EXPERIENCE_SAMPLING_AUTOMATICALLY_OPENED', + ExperienceSamplingAnswered = 'EXPERIENCE_SAMPLING_ANSWERED', + ExperienceSamplingSkipped = 'EXPERIENCE_SAMPLING_SKIPPED' +} diff --git a/src/electron/electron/ipc/IpcHandler.ts b/src/electron/electron/ipc/IpcHandler.ts index 0e774681..549d5b6b 100644 --- a/src/electron/electron/ipc/IpcHandler.ts +++ b/src/electron/electron/ipc/IpcHandler.ts @@ -93,8 +93,8 @@ export class IpcHandler { ); } - private closeExperienceSamplingWindow(): void { - this.windowService.closeExperienceSamplingWindow(); + private closeExperienceSamplingWindow(skippedExperienceSampling: boolean): void { + this.windowService.closeExperienceSamplingWindow(skippedExperienceSampling); } private closeOnboardingWindow(): void { diff --git a/src/electron/electron/main/entities/UsageDataEntity.ts b/src/electron/electron/main/entities/UsageDataEntity.ts new file mode 100644 index 00000000..c83fb89a --- /dev/null +++ b/src/electron/electron/main/entities/UsageDataEntity.ts @@ -0,0 +1,15 @@ +import { Column, Entity } from 'typeorm'; +import BaseTrackedEntity from './BaseTrackedEntity'; +import { UsageDataEventType } from '../../enums/UsageDataEventType.enum'; + +@Entity({ name: 'usage_data' }) +export class UsageDataEntity extends BaseTrackedEntity { + @Column({ + type: 'simple-enum', + enum: UsageDataEventType + }) + type: UsageDataEventType; + + @Column('text', { nullable: true }) + additionalInformation: string; +} diff --git a/src/electron/electron/main/index.ts b/src/electron/electron/main/index.ts index 1b43763e..f09c0645 100644 --- a/src/electron/electron/main/index.ts +++ b/src/electron/electron/main/index.ts @@ -18,6 +18,8 @@ import { ExperienceSamplingService } from './services/ExperienceSamplingService' import studyConfig from '../../shared/study.config'; import { is } from './services/utils/helpers'; import { Settings } from './entities/Settings'; +import { UsageDataService } from './services/UsageDataService'; +import { UsageDataEventType } from '../enums/UsageDataEventType.enum'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -71,6 +73,25 @@ app.whenReady().then(async () => { await settingsService.init(); await windowService.init(); ipcHandler.init(); + + const currentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const currentLocale = app.getLocale(); + const currentDateUTC = new Date(); + const appVersion = app.getVersion(); + const startupData = { + appVersion, + currentTimeZone, + currentLocale, + currentDateUTC + }; + LOG.info( + `App started (Version: ${appVersion}). Timezone: ${currentTimeZone}, Locale: ${currentLocale}, UTC: ${currentDateUTC}` + ); + UsageDataService.createNewUsageDataEvent( + UsageDataEventType.AppStart, + JSON.stringify(startupData) + ); + await appUpdaterService.checkForUpdates({ silent: true }); appUpdaterService.startCheckForUpdatesInterval(); @@ -121,23 +142,38 @@ app.whenReady().then(async () => { powerMonitor.on('suspend', async (): Promise => { LOG.debug('The system is going to sleep'); - await trackers.stopAllTrackers(); + await Promise.all([ + trackers.stopAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.SystemSuspend) + ]); }); powerMonitor.on('resume', async (): Promise => { LOG.debug('The system is resuming'); - await trackers.resumeAllTrackers(); + await Promise.all([ + trackers.resumeAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.SystemResume) + ]); }); powerMonitor.on('shutdown', async (): Promise => { LOG.debug('The system is going to shutdown'); - await trackers.stopAllTrackers(); + await Promise.all([ + trackers.stopAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.SystemShutdown) + ]); }); powerMonitor.on('lock-screen', async (): Promise => { LOG.debug('The system is going to lock-screen'); - await trackers.stopAllTrackers(); + await Promise.all([ + trackers.stopAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.SystemLockScreen) + ]); }); powerMonitor.on('unlock-screen', async (): Promise => { LOG.debug('The system is going to unlock-screen'); - await trackers.resumeAllTrackers(); + await Promise.all([ + trackers.resumeAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.SystemUnlockScreen) + ]); }); } } catch (error) { @@ -155,7 +191,10 @@ app.on('before-quit', async (event): Promise => { if (!isAppQuitting) { event.preventDefault(); LOG.info(`Stopping all (${trackers.getRunningTrackerNames().join(', ')}) trackers...`); - await trackers.stopAllTrackers(); + await Promise.all([ + trackers.stopAllTrackers(), + UsageDataService.createNewUsageDataEvent(UsageDataEventType.AppQuit) + ]); LOG.info(`All trackers stopped. Running: ${trackers.getRunningTrackerNames().length}`); isAppQuitting = true; app.exit(); diff --git a/src/electron/electron/main/services/DataExportService.ts b/src/electron/electron/main/services/DataExportService.ts index 2520d3ea..9fb3140a 100644 --- a/src/electron/electron/main/services/DataExportService.ts +++ b/src/electron/electron/main/services/DataExportService.ts @@ -8,6 +8,8 @@ import Database from 'better-sqlite3-multiple-ciphers'; import { WindowActivityEntity } from '../entities/WindowActivityEntity'; import { WindowActivityTrackerService } from './trackers/WindowActivityTrackerService'; import { Settings } from '../entities/Settings'; +import { UsageDataService } from './UsageDataService'; +import { UsageDataEventType } from '../../enums/UsageDataEventType.enum'; const LOG = getLogger('DataExportService'); @@ -20,6 +22,14 @@ export class DataExportService { obfuscationTerms: string[] ): Promise { LOG.info('startDataExport called'); + await UsageDataService.createNewUsageDataEvent( + UsageDataEventType.StartExport, + JSON.stringify({ + windowActivityExportType, + userInputExportType, + obfuscationTermLength: obfuscationTerms?.length + }) + ); try { const dbName = 'database.sqlite'; let dbPath = dbName; @@ -55,17 +65,17 @@ export class DataExportService { db.pragma(`rekey='PersonalAnalytics_${settings.subjectId}'`); - if (windowActivityExportType === DataExportType.Obfuscate) { + if ( + windowActivityExportType === DataExportType.Obfuscate || + DataExportType.ObfuscateWithTerms + ) { const items: { windowTitle: string; - processName: string; - processPath: string; - processId: string; url: string; id: string; }[] = await WindowActivityEntity.getRepository() .createQueryBuilder('window_activity') - .select('id, windowTitle, url, processName, processPath, processId') + .select('id, windowTitle, url') .getRawMany(); for (const item of items) { if (windowActivityExportType === DataExportType.Obfuscate) { @@ -73,24 +83,10 @@ export class DataExportService { item.windowTitle ); const randomizeUrl = this.windowActivityTrackerService.randomizeUrl(item.url); - const randomizeProcessName = this.windowActivityTrackerService.randomizeString( - item.processName - ); - const randomizeProcessPath = this.windowActivityTrackerService.randomizeString( - item.processPath - ); - const randomizeProcessId = undefined; const obfuscateWindowActivities = db.prepare( - 'UPDATE window_activity SET windowTitle = ?, url = ?, processName = ?, processPath = ?, processId = ? WHERE id = ?' - ); - obfuscateWindowActivities.run( - randomizeWindowTitle, - randomizeUrl, - randomizeProcessName, - randomizeProcessPath, - randomizeProcessId, - item.id + 'UPDATE window_activity SET windowTitle = ?, url = ? WHERE id = ?' ); + obfuscateWindowActivities.run(randomizeWindowTitle, randomizeUrl, item.id); } else if ( windowActivityExportType === DataExportType.ObfuscateWithTerms && obfuscationTerms.length > 0 @@ -101,27 +97,15 @@ export class DataExportService { lowerCaseObfuscationTerms.forEach((term: string) => { if ( item.windowTitle?.toLowerCase().includes(term) || - item.url?.toLowerCase().includes(term) || - item.processName?.toLowerCase().includes(term) || - item.processPath?.toLowerCase().includes(term) + item.url?.toLowerCase().includes(term) ) { const obfuscateWindowActivities = db.prepare( - 'UPDATE window_activity SET windowTitle = ?, url = ?, processName = ?, processPath = ?, processId = ? WHERE id = ?' + 'UPDATE window_activity SET windowTitle = ?, url = ? WHERE id = ?' ); const windowTitle = item.windowTitle ? '[anonymized]' : undefined; const url = item.url ? '[anonymized]' : undefined; - const processName = item.processName ? '[anonymized]' : undefined; - const processPath = item.processPath ? '[anonymized]' : undefined; - const processId = undefined; - obfuscateWindowActivities.run( - windowTitle, - url, - processName, - processPath, - processId, - item.id - ); + obfuscateWindowActivities.run(windowTitle, url, item.id); } }); } @@ -140,6 +124,8 @@ export class DataExportService { db.close(); + await UsageDataService.createNewUsageDataEvent(UsageDataEventType.FinishExport); + return exportDbPath; } catch (error) { LOG.error('Error exporting the data', error); diff --git a/src/electron/electron/main/services/DatabaseService.ts b/src/electron/electron/main/services/DatabaseService.ts index b6ad42aa..ab93cd67 100644 --- a/src/electron/electron/main/services/DatabaseService.ts +++ b/src/electron/electron/main/services/DatabaseService.ts @@ -7,6 +7,7 @@ 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'); @@ -27,7 +28,13 @@ export class DatabaseService { database: dbPath, synchronize: true, logging: false, - entities: [ExperienceSamplingResponseEntity, Settings, UserInputEntity, WindowActivityEntity] + entities: [ + ExperienceSamplingResponseEntity, + Settings, + UsageDataEntity, + UserInputEntity, + WindowActivityEntity + ] }; this.dataSource = new DataSource(this.options); diff --git a/src/electron/electron/main/services/UsageDataService.ts b/src/electron/electron/main/services/UsageDataService.ts new file mode 100644 index 00000000..df96040f --- /dev/null +++ b/src/electron/electron/main/services/UsageDataService.ts @@ -0,0 +1,18 @@ +import { getLogger } from '../../shared/Logger'; +import { UsageDataEventType } from '../../enums/UsageDataEventType.enum'; +import { UsageDataEntity } from '../entities/UsageDataEntity'; + +const LOG = getLogger('UsageDataService'); + +export class UsageDataService { + public static async createNewUsageDataEvent( + type: UsageDataEventType, + additionalInformation?: string + ): Promise { + LOG.debug(`Creating new usage data event of type ${type}`); + await UsageDataEntity.save({ + type, + additionalInformation + }); + } +} diff --git a/src/electron/electron/main/services/WindowService.ts b/src/electron/electron/main/services/WindowService.ts index 786dd83e..dbeb2d85 100644 --- a/src/electron/electron/main/services/WindowService.ts +++ b/src/electron/electron/main/services/WindowService.ts @@ -8,6 +8,8 @@ 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'); @@ -38,8 +40,16 @@ export class WindowService { this.createTray(); } - public async createExperienceSamplingWindow() { - this.closeExperienceSamplingWindow(); + public async createExperienceSamplingWindow(isManuallyTriggered: boolean = false) { + if (this.experienceSamplingWindow) { + this.experienceSamplingWindow.close(); + this.experienceSamplingWindow = null; + } + + const usageDataEvent = isManuallyTriggered + ? UsageDataEventType.ExperienceSamplingManuallyOpened + : UsageDataEventType.ExperienceSamplingAutomaticallyOpened; + UsageDataService.createNewUsageDataEvent(usageDataEvent); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -95,7 +105,12 @@ export class WindowService { }); } - public closeExperienceSamplingWindow() { + public closeExperienceSamplingWindow(skippedExperienceSampling: boolean) { + const usageDataEvent = skippedExperienceSampling + ? UsageDataEventType.ExperienceSamplingSkipped + : UsageDataEventType.ExperienceSamplingAnswered; + UsageDataService.createNewUsageDataEvent(usageDataEvent); + if (this.experienceSamplingWindow) { this.experienceSamplingWindow.close(); this.experienceSamplingWindow = null; @@ -218,6 +233,7 @@ export class WindowService { show: false, minimizable: false, maximizable: false, + minWidth: 1200, minHeight: 850, fullscreenable: false, title: 'PersonalAnalytics: Data Export', @@ -239,7 +255,7 @@ export class WindowService { return { action: 'deny' }; }); - if (!is.dev) { + if (is.macOS && !is.dev) { const template = [ { label: 'Edit', @@ -305,7 +321,7 @@ export class WindowService { const windowMenu: MenuItemConstructorOptions[] = [ { label: 'Open Experience Sampling', - click: () => this.createExperienceSamplingWindow() + click: () => this.createExperienceSamplingWindow(true) }, { label: 'Open Onboarding', diff --git a/src/electron/electron/main/services/trackers/WindowActivityTrackerService.ts b/src/electron/electron/main/services/trackers/WindowActivityTrackerService.ts index cda6cfc9..252fd6e0 100644 --- a/src/electron/electron/main/services/trackers/WindowActivityTrackerService.ts +++ b/src/electron/electron/main/services/trackers/WindowActivityTrackerService.ts @@ -58,11 +58,11 @@ export class WindowActivityTrackerService { ).map((activity) => { return { windowTitle: this.randomizeString(activity.windowTitle), - processName: this.randomizeString(activity.processName), - processPath: this.randomizeString(activity.processPath), - processId: undefined, url: this.randomizeUrl(activity.url), - activity: this.randomizeString(activity.activity), + processName: activity.processName, + processPath: activity.processPath, + processId: activity.processId, + activity: activity.activity, ts: activity.ts, id: activity.id, createdAt: activity.createdAt, diff --git a/src/electron/package.json b/src/electron/package.json index 78b3d12d..d730b400 100644 --- a/src/electron/package.json +++ b/src/electron/package.json @@ -1,6 +1,6 @@ { "name": "personal-analytics", - "version": "0.0.18", + "version": "0.0.19", "main": "dist-electron/main/index.js", "type": "module", "author": { @@ -10,6 +10,7 @@ "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build && electron-builder --publish always", + "build:mac": "vue-tsc --noEmit && vite build && electron-builder --mac", "build:win": "vue-tsc --noEmit && vite build && electron-builder --win", "postinstall": "electron-builder install-app-deps", "preview": "vite preview", diff --git a/src/electron/src/utils/Commands.ts b/src/electron/src/utils/Commands.ts index 0197cd08..bacd5947 100644 --- a/src/electron/src/utils/Commands.ts +++ b/src/electron/src/utils/Commands.ts @@ -13,7 +13,7 @@ type Commands = { response?: number, skipped?: boolean ) => Promise; - closeExperienceSamplingWindow: () => void; + closeExperienceSamplingWindow: (skippedExperienceSampling: boolean) => void; closeOnboardingWindow: () => void; closeDataExportWindow: () => void; getStudyInfo: () => Promise; diff --git a/src/electron/src/views/DataExportView.vue b/src/electron/src/views/DataExportView.vue index 6467408b..49b07c1f 100644 --- a/src/electron/src/views/DataExportView.vue +++ b/src/electron/src/views/DataExportView.vue @@ -71,9 +71,6 @@ onMounted(async () => { }); async function handleWindowActivityExportConfigChanged(newSelectedOption: DataExportType) { - if (newSelectedOption !== DataExportType.ObfuscateWithTerms) { - obfuscationTermsInput.value = []; - } if (mostRecentWindowActivities.value && newSelectedOption === DataExportType.Obfuscate) { mostRecentWindowActivitiesObfuscated.value = await typedIpcRenderer.invoke( 'obfuscateWindowActivityDtosById', @@ -110,24 +107,16 @@ async function handleObfuscateSampleData() { mostRecentWindowActivities.value = mostRecentWindowActivities.value?.map((item) => { let windowTitle = item.windowTitle; let url = item.url; - let processName = item.processName; - let processPath = item.processPath; - let processId = item.processId; obfuscationTermsInput.value?.forEach((term) => { if ( windowTitle?.toLowerCase().includes(term.toLowerCase()) || - url?.toLowerCase().includes(term.toLowerCase()) || - processName?.toLowerCase().includes(term.toLowerCase()) || - processPath?.toLowerCase().includes(term.toLowerCase()) + url?.toLowerCase().includes(term.toLowerCase()) ) { windowTitle = windowTitle ? '[anonymized]' : windowTitle; url = url ? '[anonymized]' : url; - processName = processName ? '[anonymized]' : processName; - processPath = processPath ? '[anonymized]' : processPath; - processId = processId ? null : processId; } }); - return { ...item, windowTitle, url, processPath, processName, processId }; + return { ...item, windowTitle, url }; }); } else { mostRecentWindowActivities.value = await typedIpcRenderer.invoke( @@ -160,7 +149,14 @@ async function handleNextStep() { if (currentNamedStep.value === 'create-export') { isExporting.value = true; try { - const obfuscationTerms = Array.from(obfuscationTermsInput.value || []); + let obfuscationTerms: string[] = []; + if ( + exportWindowActivitySelectedOption.value === DataExportType.ObfuscateWithTerms && + obfuscationTermsInput.value && + obfuscationTermsInput.value.length > 0 + ) { + obfuscationTerms = Array.from(obfuscationTermsInput.value); + } pathToExportedFile.value = await typedIpcRenderer.invoke( 'startDataExport', exportWindowActivitySelectedOption.value, diff --git a/src/electron/src/views/ExperienceSamplingView.vue b/src/electron/src/views/ExperienceSamplingView.vue index 2f776743..088eb566 100644 --- a/src/electron/src/views/ExperienceSamplingView.vue +++ b/src/electron/src/views/ExperienceSamplingView.vue @@ -30,7 +30,7 @@ async function createExperienceSample(value: number) { ), new Promise((resolve) => setTimeout(resolve, 150)) ]); - await typedIpcRenderer.invoke('closeExperienceSamplingWindow'); + await typedIpcRenderer.invoke('closeExperienceSamplingWindow', false); } catch (error) { console.error('Error creating team', error); } @@ -51,7 +51,7 @@ async function skipExperienceSample() { ), new Promise((resolve) => setTimeout(resolve, 150)) ]); - await typedIpcRenderer.invoke('closeExperienceSamplingWindow'); + await typedIpcRenderer.invoke('closeExperienceSamplingWindow', true); } catch (error) { console.error('Error creating team', error); }