Merge branch 'master' into heartbeat-status

This commit is contained in:
Alan Hamlett
2024-10-09 00:12:43 +02:00
committed by GitHub
12 changed files with 118 additions and 72 deletions

View File

@@ -60,14 +60,6 @@ browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
} }
}); });
/**
* Creates IndexedDB
* https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
self.addEventListener('activate', async () => {
await WakaTimeCore.db();
});
browser.runtime.onMessage.addListener(async (request: { task: string }, sender) => { browser.runtime.onMessage.addListener(async (request: { task: string }, sender) => {
if (request.task === 'handleActivity') { if (request.task === 'handleActivity') {
if (!sender.tab?.id) return; if (!sender.tab?.id) return;

View File

@@ -27,6 +27,27 @@ describe('MainList', () => {
expect(container).toMatchInlineSnapshot(` expect(container).toMatchInlineSnapshot(`
<div> <div>
<div> <div>
<div
class="placeholder-glow"
>
<span
class="placeholder col-12"
/>
</div>
<div
class="placeholder-glow"
>
<span
class="placeholder col-12"
/>
</div>
<div
class="placeholder-glow"
>
<span
class="placeholder col-12"
/>
</div>
<div <div
class="list-group" class="list-group"
> >
@@ -39,17 +60,6 @@ describe('MainList', () => {
/> />
Options Options
</a> </a>
<a
class="list-group-item text-body-secondary"
href="https://wakatime.com/login"
rel="noreferrer"
target="_blank"
>
<i
class="fa fa-fw fa-sign-in me-2"
/>
Login
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import React from 'react';
import { configLogout, setLoggingEnabled } from '../reducers/configReducer'; import { configLogout, setLoggingEnabled } from '../reducers/configReducer';
import { userLogout } from '../reducers/currentUser'; import { userLogout } from '../reducers/currentUser';
import { ReduxSelector } from '../types/store'; import { ReduxSelector } from '../types/store';
@@ -23,6 +24,9 @@ export default function MainList({
const user: User | undefined = useSelector( const user: User | undefined = useSelector(
(selector: ReduxSelector) => selector.currentUser.user, (selector: ReduxSelector) => selector.currentUser.user,
); );
const isLoading: boolean = useSelector(
(selector: ReduxSelector) => selector.currentUser.pending ?? true,
);
const logoutUser = async (): Promise<void> => { const logoutUser = async (): Promise<void> => {
await browser.storage.sync.set({ apiKey: '' }); await browser.storage.sync.set({ apiKey: '' });
@@ -43,6 +47,12 @@ export default function MainList({
await changeExtensionState('trackingDisabled'); await changeExtensionState('trackingDisabled');
}; };
const loading = isLoading ? (
<div className="placeholder-glow">
<span className="placeholder col-12"></span>
</div>
) : null;
return ( return (
<div> <div>
{user ? ( {user ? (
@@ -56,7 +66,9 @@ export default function MainList({
</blockquote> </blockquote>
</div> </div>
</div> </div>
) : null} ) : (
loading
)}
{loggingEnabled && user ? ( {loggingEnabled && user ? (
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
@@ -71,7 +83,9 @@ export default function MainList({
</p> </p>
</div> </div>
</div> </div>
) : null} ) : (
loading
)}
{!loggingEnabled && user ? ( {!loggingEnabled && user ? (
<div className="row"> <div className="row">
<div className="col-xs-12"> <div className="col-xs-12">
@@ -86,21 +100,22 @@ export default function MainList({
</p> </p>
</div> </div>
</div> </div>
) : null} ) : (
loading
)}
<div className="list-group"> <div className="list-group">
<a href="#" className="list-group-item text-body-secondary" onClick={openOptionsPage}> <a href="#" className="list-group-item text-body-secondary" onClick={openOptionsPage}>
<i className="fa fa-fw fa-cogs me-2" /> <i className="fa fa-fw fa-cogs me-2" />
Options Options
</a> </a>
{user ? ( {isLoading ? null : user ? (
<div> <div>
<a href="#" className="list-group-item text-body-secondary" onClick={logoutUser}> <a href="#" className="list-group-item text-body-secondary" onClick={logoutUser}>
<i className="fa fa-fw fa-sign-out me-2" /> <i className="fa fa-fw fa-sign-out me-2" />
Logout Logout
</a> </a>
</div> </div>
) : null} ) : (
{user ? null : (
<a <a
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"

View File

@@ -52,9 +52,6 @@ export default function Options(): JSX.Element {
const handleSubmit = async () => { const handleSubmit = async () => {
if (state.loading) return; if (state.loading) return;
setState((oldState) => ({ ...oldState, loading: true })); setState((oldState) => ({ ...oldState, loading: true }));
if (state.apiUrl.endsWith('/')) {
state.apiUrl = state.apiUrl.slice(0, -1);
}
await saveSettings({ await saveSettings({
allowList: state.allowList.filter((item) => !!item.trim()), allowList: state.allowList.filter((item) => !!item.trim()),
apiKey: state.apiKey, apiKey: state.apiKey,

View File

@@ -58,7 +58,7 @@ describe('wakatime config', () => {
"chrome://", "chrome://",
"about:", "about:",
], ],
"queueName": "heartbeatQueue", "queueName": "heartbeatsQueue",
"socialMediaSites": [ "socialMediaSites": [
"facebook.com", "facebook.com",
"instagram.com", "instagram.com",

View File

@@ -164,7 +164,7 @@ const config: Config = {
nonTrackableSites: ['chrome://', 'about:'], nonTrackableSites: ['chrome://', 'about:'],
queueName: 'heartbeatQueue', queueName: 'heartbeatsQueue',
socialMediaSites: [ socialMediaSites: [
'facebook.com', 'facebook.com',

View File

@@ -1,4 +1,4 @@
import { IDBPDatabase, openDB } from 'idb'; import { openDB } from 'idb';
import browser, { Tabs } from 'webextension-polyfill'; import browser, { Tabs } from 'webextension-polyfill';
/* eslint-disable no-fallthrough */ /* eslint-disable no-fallthrough */
/* eslint-disable default-case */ /* eslint-disable default-case */
@@ -9,6 +9,7 @@ import { changeExtensionStatus } from '../utils/changeExtensionStatus';
import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl'; import getDomainFromUrl, { getDomain } from '../utils/getDomainFromUrl';
import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils/operatingSystem'; import { getOperatingSystem, IS_EDGE, IS_FIREFOX } from '../utils/operatingSystem';
import { getSettings, Settings } from '../utils/settings'; import { getSettings, Settings } from '../utils/settings';
import { getApiUrl } from '../utils/user';
import config, { ExtensionStatus } from '../config/config'; import config, { ExtensionStatus } from '../config/config';
import { EntityType, Heartbeat, HeartbeatsBulkResponse } from '../types/heartbeats'; import { EntityType, Heartbeat, HeartbeatsBulkResponse } from '../types/heartbeats';
@@ -18,7 +19,6 @@ class WakaTimeCore {
lastHeartbeat: Heartbeat | undefined; lastHeartbeat: Heartbeat | undefined;
lastHeartbeatSentAt = 0; lastHeartbeatSentAt = 0;
lastExtensionState: ExtensionStatus = 'allGood'; lastExtensionState: ExtensionStatus = 'allGood';
_db: IDBPDatabase | undefined;
constructor() { constructor() {
this.tabsWithDevtoolsOpen = []; this.tabsWithDevtoolsOpen = [];
} }
@@ -28,17 +28,13 @@ class WakaTimeCore {
* a library that adds promises to IndexedDB and makes it easy to use * a library that adds promises to IndexedDB and makes it easy to use
*/ */
async db() { async db() {
if (!this._db) { return openDB('wakatime', 2, {
const dbConnection = await openDB('wakatime', 1, {
upgrade(db) { upgrade(db) {
db.createObjectStore(config.queueName, { db.createObjectStore(config.queueName, {
keyPath: 'id', keyPath: 'id',
}); });
}, },
}); });
this._db = dbConnection;
}
return this._db;
} }
shouldSendHeartbeat(heartbeat: Heartbeat): boolean { shouldSendHeartbeat(heartbeat: Heartbeat): boolean {
@@ -171,7 +167,6 @@ class WakaTimeCore {
async sendHeartbeats(): Promise<void> { async sendHeartbeats(): Promise<void> {
const settings = await browser.storage.sync.get({ const settings = await browser.storage.sync.get({
apiKey: config.apiKey, apiKey: config.apiKey,
apiUrl: config.apiUrl,
heartbeatApiEndPoint: config.heartbeatApiEndPoint, heartbeatApiEndPoint: config.heartbeatApiEndPoint,
hostname: '', hostname: '',
}); });
@@ -180,16 +175,8 @@ class WakaTimeCore {
return; return;
} }
const heartbeats = (await (await this.db()).getAll(config.queueName, undefined, 50)) as const heartbeats = await this.getHeartbeatsFromQueue();
| Heartbeat[] if (heartbeats.length === 0) return;
| undefined;
if (!heartbeats || heartbeats.length === 0) return;
await Promise.all(
heartbeats.map(async (heartbeat) => {
return (await this.db()).delete(config.queueName, heartbeat.id);
}),
);
const userAgent = await this.getUserAgent(); const userAgent = await this.getUserAgent();
@@ -209,7 +196,8 @@ class WakaTimeCore {
}; };
} }
const url = `${settings.apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`; const apiUrl = await getApiUrl();
const url = `${apiUrl}${settings.heartbeatApiEndPoint}?api_key=${settings.apiKey}`;
const response = await fetch(url, request); const response = await fetch(url, request);
if (response.status === 401) { if (response.status === 401) {
await this.putHeartbeatsBackInQueue(heartbeats); await this.putHeartbeatsBackInQueue(heartbeats);
@@ -228,7 +216,7 @@ class WakaTimeCore {
if (resp[0].error) { if (resp[0].error) {
await this.putHeartbeatsBackInQueue(heartbeats.filter((h, i) => i === respNumber)); await this.putHeartbeatsBackInQueue(heartbeats.filter((h, i) => i === respNumber));
console.error(resp[0].error); console.error(resp[0].error);
} else if ((resp[1] === 201 || resp[1] === 202) && resp[0].data?.id) { } else if (resp[1] === 201 || resp[1] === 202) {
await changeExtensionStatus('allGood'); await changeExtensionStatus('allGood');
} else { } else {
if (resp[1] !== 400) { if (resp[1] !== 400) {
@@ -251,10 +239,38 @@ class WakaTimeCore {
} }
} }
async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise<void> { async getHeartbeatsFromQueue(): Promise<Heartbeat[]> {
const tx = (await this.db()).transaction(config.queueName, 'readwrite');
const heartbeats = (await tx.store.getAll(undefined, 25)) as Heartbeat[] | undefined;
if (!heartbeats || heartbeats.length === 0) return [];
await Promise.all( await Promise.all(
heartbeats.map(async (heartbeat) => (await this.db()).add(config.queueName, heartbeat)), heartbeats.map(async (heartbeat) => {
return tx.store.delete(heartbeat.id);
}),
); );
await tx.done;
return heartbeats;
}
async putHeartbeatsBackInQueue(heartbeats: Heartbeat[]): Promise<void> {
await Promise.all(heartbeats.map(async (heartbeat) => this.putHeartbeatBackInQueue(heartbeat)));
}
async putHeartbeatBackInQueue(heartbeat: Heartbeat, tries = 0): Promise<void> {
try {
await (await this.db()).add(config.queueName, heartbeat);
} catch (err: unknown) {
if (tries < 10) {
return await this.putHeartbeatBackInQueue(heartbeat, tries + 1);
}
console.error(err);
console.error(`Unable to add heartbeat back into queue: ${heartbeat.id}`);
console.error(JSON.stringify(heartbeat));
}
} }
async getUserAgent(): Promise<string> { async getUserAgent(): Promise<string> {

View File

@@ -33,5 +33,5 @@
"page": "options.html" "page": "options.html"
}, },
"permissions": ["alarms", "tabs", "storage", "activeTab"], "permissions": ["alarms", "tabs", "storage", "activeTab"],
"version": "4.0.2" "version": "4.0.6"
} }

View File

@@ -33,5 +33,5 @@
"page": "options.html" "page": "options.html"
}, },
"permissions": ["alarms", "tabs", "storage", "activeTab"], "permissions": ["alarms", "tabs", "storage", "activeTab"],
"version": "4.0.2" "version": "4.0.6"
} }

View File

@@ -39,5 +39,5 @@
"page": "options.html" "page": "options.html"
}, },
"permissions": ["alarms", "tabs", "storage", "activeTab"], "permissions": ["alarms", "tabs", "storage", "activeTab"],
"version": "4.0.2" "version": "4.0.6"
} }

View File

@@ -3,6 +3,7 @@ import axios, { AxiosResponse } from 'axios';
import browser from 'webextension-polyfill'; import browser from 'webextension-polyfill';
import config from '../config/config'; import config from '../config/config';
import { CurrentUser, User, UserPayload } from '../types/user'; import { CurrentUser, User, UserPayload } from '../types/user';
import { getApiUrl } from '../utils/user';
interface setUserAction { interface setUserAction {
payload: User | undefined; payload: User | undefined;
@@ -16,11 +17,11 @@ export const fetchCurrentUser = createAsyncThunk<User, string>(
`[${name}]`, `[${name}]`,
async (api_key = '') => { async (api_key = '') => {
const items = await browser.storage.sync.get({ const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint, currentUserApiEndPoint: config.currentUserApiEndPoint,
}); });
const apiUrl = await getApiUrl();
const userPayload: AxiosResponse<UserPayload> = await axios.get( const userPayload: AxiosResponse<UserPayload> = await axios.get(
`${items.apiUrl}${items.currentUserApiEndPoint}`, `${apiUrl}${items.currentUserApiEndPoint}`,
{ {
params: { api_key }, params: { api_key },
}, },
@@ -35,10 +36,12 @@ const currentUser = createSlice({
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(fetchCurrentUser.fulfilled, (state, { payload }) => { builder.addCase(fetchCurrentUser.fulfilled, (state, { payload }) => {
state.user = payload; state.user = payload;
state.pending = false;
}); });
builder.addCase(fetchCurrentUser.rejected, (state, { error }) => { builder.addCase(fetchCurrentUser.rejected, (state, { error }) => {
state.user = undefined; state.user = undefined;
state.error = error; state.error = error;
state.pending = false;
}); });
}, },
initialState, initialState,

View File

@@ -1,5 +1,6 @@
import { AnyAction, Dispatch } from '@reduxjs/toolkit'; import { AnyAction, Dispatch } from '@reduxjs/toolkit';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import browser from 'webextension-polyfill';
import moment from 'moment'; import moment from 'moment';
import config from '../config/config'; import config from '../config/config';
@@ -9,6 +10,20 @@ import { GrandTotal, Summaries } from '../types/summaries';
import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user'; import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user';
import changeExtensionState from './changeExtensionStatus'; import changeExtensionState from './changeExtensionStatus';
export const getApiUrl = async () => {
const settings = await browser.storage.sync.get({
apiUrl: config.apiUrl,
});
let apiUrl = (settings.apiUrl as string) || config.apiUrl;
const suffixes = ['/', '.bulk', '/users/current/heartbeats', '/heartbeats', '/heartbeat'];
for (const suffix of suffixes) {
if (apiUrl.endsWith(suffix)) {
apiUrl = apiUrl.slice(0, -suffix.length);
}
}
return apiUrl;
};
/** /**
* Checks if the user is logged in. * Checks if the user is logged in.
* *
@@ -16,11 +31,11 @@ import changeExtensionState from './changeExtensionStatus';
*/ */
const checkAuth = async (api_key = ''): Promise<User> => { const checkAuth = async (api_key = ''): Promise<User> => {
const items = await browser.storage.sync.get({ const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint, currentUserApiEndPoint: config.currentUserApiEndPoint,
}); });
const apiUrl = await getApiUrl();
const userPayload: AxiosResponse<AxiosUserResponse> = await axios.get( const userPayload: AxiosResponse<AxiosUserResponse> = await axios.get(
`${items.apiUrl}${items.currentUserApiEndPoint}`, `${apiUrl}${items.currentUserApiEndPoint}`,
{ params: { api_key } }, { params: { api_key } },
); );
return userPayload.data.data; return userPayload.data.data;
@@ -54,12 +69,11 @@ export const logUserIn = async (apiKey: string): Promise<void> => {
const fetchApiKey = async (): Promise<string> => { const fetchApiKey = async (): Promise<string> => {
try { try {
const items = await browser.storage.sync.get({ const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
currentUserApiEndPoint: config.currentUserApiEndPoint, currentUserApiEndPoint: config.currentUserApiEndPoint,
}); });
const apiUrl = await getApiUrl();
const apiKeyResponse: AxiosResponse<ApiKeyPayload> = await axios.post( const apiKeyResponse: AxiosResponse<ApiKeyPayload> = await axios.post(
`${items.apiUrl}${items.currentUserApiEndPoint}/get_api_key`, `${apiUrl}${items.currentUserApiEndPoint}/get_api_key`,
); );
return apiKeyResponse.data.data.api_key; return apiKeyResponse.data.data.api_key;
} catch (err: unknown) { } catch (err: unknown) {
@@ -69,13 +83,12 @@ const fetchApiKey = async (): Promise<string> => {
const getTotalTimeLoggedToday = async (api_key = ''): Promise<GrandTotal> => { const getTotalTimeLoggedToday = async (api_key = ''): Promise<GrandTotal> => {
const items = await browser.storage.sync.get({ const items = await browser.storage.sync.get({
apiUrl: config.apiUrl,
summariesApiEndPoint: config.summariesApiEndPoint, summariesApiEndPoint: config.summariesApiEndPoint,
}); });
const apiUrl = await getApiUrl();
const today = moment().format('YYYY-MM-DD'); const today = moment().format('YYYY-MM-DD');
const summariesAxiosPayload: AxiosResponse<Summaries> = await axios.get( const summariesAxiosPayload: AxiosResponse<Summaries> = await axios.get(
`${items.apiUrl}${items.summariesApiEndPoint}`, `${apiUrl}${items.summariesApiEndPoint}`,
{ {
params: { params: {
api_key, api_key,