From 3f4daed0caade15f29183ef4b034a8e761ef431c Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Wed, 18 Jan 2023 14:26:47 -0500 Subject: [PATCH 01/12] chore: implements send Heartbeat functionality --- src/core/WakaTimeCore.ts | 209 +++++++++++++++++++++++++++++++++++++++ src/types/heartbeats.ts | 5 + src/utils/inArray.ts | 2 +- 3 files changed, 215 insertions(+), 1 deletion(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 9f7923f..0add85c 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -4,6 +4,11 @@ import { Tabs } from 'webextension-polyfill'; import { AxiosUserResponse, User } from '../types/user'; import config from '../config/config'; import { SummariesPayload, GrandTotal } from '../types/summaries'; +import changeExtensionState from '../utils/changeExtensionState'; +import inArray from '../utils/inArray'; +import contains from '../utils/contains'; +import { SendHeartbeat } from '../types/heartbeats'; +import getDomainFromUrl from '../utils/getDomainFromUrl'; class WakaTimeCore { tabsWithDevtoolsOpen: Tabs.Tab[]; @@ -30,6 +35,11 @@ class WakaTimeCore { return summariesAxiosPayload.data.data[0].grand_total; } + /** + * Checks if the user is logged in. + * + * @returns {*} + */ async checkAuth(api_key = ''): Promise { const userPayload: AxiosResponse = await axios.get( config.currentUserApiUrl, @@ -38,6 +48,10 @@ class WakaTimeCore { return userPayload.data.data; } + /** + * Depending on various factors detects the current active tab URL or domain, + * and sends it to WakaTime for logging. + */ async recordHeartbeat(): Promise { const items = await browser.storage.sync.get({ blacklist: '', @@ -45,6 +59,201 @@ class WakaTimeCore { loggingStyle: config.loggingStyle, whitelist: '', }); + if (items.loggingEnabled === true) { + await changeExtensionState('allGood'); + + const newState = await browser.idle.queryState(config.detectionIntervalInSeconds); + + if (newState === 'active') { + // Get current tab URL. + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length == 0) return; + + const currentActiveTab = tabs[0]; + let debug = false; + + // If the current active tab has devtools open + if ( + inArray( + currentActiveTab.id, + this.tabsWithDevtoolsOpen.map((tab) => tab.id), + ) + ) { + debug = true; + } + + if (items.loggingStyle == 'blacklist') { + if (!contains(currentActiveTab.url as string, items.blacklist as string)) { + await this.sendHeartbeat( + { + project: null, + url: currentActiveTab.url as string, + }, + debug, + ); + } else { + await changeExtensionState('blacklisted'); + console.log(`${currentActiveTab.url} is on a blacklist.`); + } + } + + if (items.loggingStyle == 'whitelist') { + const heartbeat = this.getHeartbeat( + currentActiveTab.url as string, + items.whitelist as string, + ); + if (heartbeat.url) { + await this.sendHeartbeat(heartbeat, debug); + } else { + await changeExtensionState('whitelisted'); + console.log(`${currentActiveTab.url} is not on a whitelist.`); + } + } + } + } else { + await changeExtensionState('notLogging'); + } + } + + /** + * Creates an array from list using \n as delimiter + * and checks if any element in list is contained in the url. + * Also checks if element is assigned to a project using @@ as delimiter + * + * @param url + * @param list + * @returns {object} + */ + getHeartbeat(url: string, list: string) { + const projectIndicatorCharacters = '@@'; + + const lines = list.split('\n'); + for (let i = 0; i < lines.length; i++) { + // strip (http:// or https://) and trailing (`/` or `@@`) + const cleanLine = lines[i] + .trim() + .replace(/(\/|@@)$/, '') + .replace(/^(?:https?:\/\/)?/i, ''); + if (cleanLine === '') continue; + + const projectIndicatorIndex = cleanLine.lastIndexOf(projectIndicatorCharacters); + const projectIndicatorExists = projectIndicatorIndex > -1; + let projectName = null; + let urlFromLine = cleanLine; + if (projectIndicatorExists) { + const start = projectIndicatorIndex + projectIndicatorCharacters.length; + projectName = cleanLine.substring(start); + urlFromLine = cleanLine + .replace(cleanLine.substring(projectIndicatorIndex), '') + .replace(/\/$/, ''); + } + const schemaHttpExists = url.match(/^http:\/\//i); + const schemaHttpsExists = url.match(/^https:\/\//i); + let schema = ''; + if (schemaHttpExists) { + schema = 'http://'; + } + if (schemaHttpsExists) { + schema = 'https://'; + } + const cleanUrl = url + .trim() + .replace(/(\/|@@)$/, '') + .replace(/^(?:https?:\/\/)?/i, ''); + const startsWithUrl = cleanUrl.toLowerCase().includes(urlFromLine.toLowerCase()); + if (startsWithUrl) { + return { + project: projectName, + url: schema + urlFromLine, + }; + } + } + + return { + project: null, + url: null, + }; + } + + /** + * Given the heartbeat and logging type it creates a payload and + * sends an ajax post request to the API. + * + * @param heartbeat + * @param debug + */ + async sendHeartbeat(heartbeat: SendHeartbeat, debug: boolean): Promise { + let payload = null; + + const loggingType = await this.getLoggingType(); + // Get only the domain from the entity. + // And send that in heartbeat + if (loggingType == 'domain') { + heartbeat.url = getDomainFromUrl(heartbeat.url); + payload = this.preparePayload(heartbeat, 'domain', debug); + console.log(payload); + await this.sendAjaxRequestToApi(payload); + } + // Send entity in heartbeat + else if (loggingType == 'url') { + payload = this.preparePayload(heartbeat, 'url', debug); + console.log(payload); + await this.sendAjaxRequestToApi(payload); + } + } + + /** + * Returns a promise with logging type variable. + * + * @returns {*} + * @private + */ + async getLoggingType(): Promise { + const items = await browser.storage.sync.get({ + loggingType: config.loggingType, + }); + + return items.loggingType; + } + + /** + * Creates payload for the heartbeat and returns it as JSON. + * + * @param heartbeat + * @param type + * @param debug + * @returns {*} + * @private + */ + preparePayload(heartbeat: SendHeartbeat, type: string, debug = false): Record { + return { + entity: heartbeat.url, + is_debugging: debug, + plugin: 'browser-wakatime/' + config.version, + project: heartbeat.project ?? '<>', + time: moment().format('X'), + type: type, + }; + } + + /** + * Sends AJAX request with payload to the heartbeat API as JSON. + * + * @param payload + * @param method + * @returns {*} + */ + async sendAjaxRequestToApi(payload: Record, api_key = '') { + try { + const response = await axios.post(config.heartbeatApiUrl, payload, { + params: { + api_key, + }, + }); + return response.data; + } catch (err: unknown) { + await changeExtensionState('notSignedIn'); + } } } diff --git a/src/types/heartbeats.ts b/src/types/heartbeats.ts index e178266..8067433 100644 --- a/src/types/heartbeats.ts +++ b/src/types/heartbeats.ts @@ -26,3 +26,8 @@ export interface Datum { user_agent_id: string; user_id: string; } + +export interface SendHeartbeat { + project: string | null; + url: string; +} diff --git a/src/utils/inArray.ts b/src/utils/inArray.ts index d9d914c..55696eb 100644 --- a/src/utils/inArray.ts +++ b/src/utils/inArray.ts @@ -1,7 +1,7 @@ /** * Returns boolean if needle is found in haystack or not. */ -export default function in_array(needle: T, haystack: T[]): boolean { +export default function inArray(needle: T, haystack: T[]): boolean { for (let i = 0; i < haystack.length; i++) { if (needle == haystack[i]) { return true; From 6547f4916b428fadb29b2bdf8ecd97d5baf7d8f3 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Thu, 19 Jan 2023 05:45:17 -0500 Subject: [PATCH 02/12] chore: send api token to heartbeat end point --- src/core/WakaTimeCore.ts | 17 ++++++++--------- src/utils/user.ts | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 0add85c..5e20e58 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -52,7 +52,7 @@ class WakaTimeCore { * Depending on various factors detects the current active tab URL or domain, * and sends it to WakaTime for logging. */ - async recordHeartbeat(): Promise { + async recordHeartbeat(apiKey: string): Promise { const items = await browser.storage.sync.get({ blacklist: '', loggingEnabled: config.loggingEnabled, @@ -90,6 +90,7 @@ class WakaTimeCore { url: currentActiveTab.url as string, }, debug, + apiKey, ); } else { await changeExtensionState('blacklisted'); @@ -103,7 +104,7 @@ class WakaTimeCore { items.whitelist as string, ); if (heartbeat.url) { - await this.sendHeartbeat(heartbeat, debug); + await this.sendHeartbeat(heartbeat, debug, apiKey); } else { await changeExtensionState('whitelisted'); console.log(`${currentActiveTab.url} is not on a whitelist.`); @@ -182,8 +183,8 @@ class WakaTimeCore { * @param heartbeat * @param debug */ - async sendHeartbeat(heartbeat: SendHeartbeat, debug: boolean): Promise { - let payload = null; + async sendHeartbeat(heartbeat: SendHeartbeat, debug: boolean, apiKey: string): Promise { + let payload; const loggingType = await this.getLoggingType(); // Get only the domain from the entity. @@ -191,14 +192,12 @@ class WakaTimeCore { if (loggingType == 'domain') { heartbeat.url = getDomainFromUrl(heartbeat.url); payload = this.preparePayload(heartbeat, 'domain', debug); - console.log(payload); - await this.sendAjaxRequestToApi(payload); + await this.sendPostRequestToApi(payload, apiKey); } // Send entity in heartbeat else if (loggingType == 'url') { payload = this.preparePayload(heartbeat, 'url', debug); - console.log(payload); - await this.sendAjaxRequestToApi(payload); + await this.sendPostRequestToApi(payload, apiKey); } } @@ -243,7 +242,7 @@ class WakaTimeCore { * @param method * @returns {*} */ - async sendAjaxRequestToApi(payload: Record, api_key = '') { + async sendPostRequestToApi(payload: Record, api_key = '') { try { const response = await axios.post(config.heartbeatApiUrl, payload, { params: { diff --git a/src/utils/user.ts b/src/utils/user.ts index d87b741..6166ac7 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -38,7 +38,7 @@ export const fetchUserData = async ( dispatch(setLoggingEnabled(items.loggingEnabled as boolean)); dispatch(setTotalTimeLoggedToday(totalTimeLoggedTodayResponse.text)); - await WakaTimeCore.recordHeartbeat(); + await WakaTimeCore.recordHeartbeat(apiKey); } catch (err: unknown) { await changeExtensionState('notSignedIn'); } From 75e6372f2bec504ea19067e2e2d008d54459c811 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Thu, 19 Jan 2023 10:04:33 -0500 Subject: [PATCH 03/12] chore: edit heartbeat payload to not send is_debugging --- src/core/WakaTimeCore.ts | 23 +++++------------------ src/utils/changeExtensionIcon.ts | 4 ++-- src/utils/changeExtensionTooltip.ts | 2 +- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 5e20e58..e2e2135 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -70,17 +70,6 @@ class WakaTimeCore { if (tabs.length == 0) return; const currentActiveTab = tabs[0]; - let debug = false; - - // If the current active tab has devtools open - if ( - inArray( - currentActiveTab.id, - this.tabsWithDevtoolsOpen.map((tab) => tab.id), - ) - ) { - debug = true; - } if (items.loggingStyle == 'blacklist') { if (!contains(currentActiveTab.url as string, items.blacklist as string)) { @@ -89,7 +78,6 @@ class WakaTimeCore { project: null, url: currentActiveTab.url as string, }, - debug, apiKey, ); } else { @@ -104,7 +92,7 @@ class WakaTimeCore { items.whitelist as string, ); if (heartbeat.url) { - await this.sendHeartbeat(heartbeat, debug, apiKey); + await this.sendHeartbeat(heartbeat, apiKey); } else { await changeExtensionState('whitelisted'); console.log(`${currentActiveTab.url} is not on a whitelist.`); @@ -183,7 +171,7 @@ class WakaTimeCore { * @param heartbeat * @param debug */ - async sendHeartbeat(heartbeat: SendHeartbeat, debug: boolean, apiKey: string): Promise { + async sendHeartbeat(heartbeat: SendHeartbeat, apiKey: string): Promise { let payload; const loggingType = await this.getLoggingType(); @@ -191,12 +179,12 @@ class WakaTimeCore { // And send that in heartbeat if (loggingType == 'domain') { heartbeat.url = getDomainFromUrl(heartbeat.url); - payload = this.preparePayload(heartbeat, 'domain', debug); + payload = this.preparePayload(heartbeat, 'domain'); await this.sendPostRequestToApi(payload, apiKey); } // Send entity in heartbeat else if (loggingType == 'url') { - payload = this.preparePayload(heartbeat, 'url', debug); + payload = this.preparePayload(heartbeat, 'url'); await this.sendPostRequestToApi(payload, apiKey); } } @@ -224,10 +212,9 @@ class WakaTimeCore { * @returns {*} * @private */ - preparePayload(heartbeat: SendHeartbeat, type: string, debug = false): Record { + preparePayload(heartbeat: SendHeartbeat, type: string): Record { return { entity: heartbeat.url, - is_debugging: debug, plugin: 'browser-wakatime/' + config.version, project: heartbeat.project ?? '<>', time: moment().format('X'), diff --git a/src/utils/changeExtensionIcon.ts b/src/utils/changeExtensionIcon.ts index a5a426f..d1e8d1a 100644 --- a/src/utils/changeExtensionIcon.ts +++ b/src/utils/changeExtensionIcon.ts @@ -10,7 +10,7 @@ export default async function changeExtensionIcon(color?: ColorIconTypes): Promi if (color) { const path = `./graphics/wakatime-logo-38-${color}.png`; - await browser.browserAction.setIcon({ + await browser.action.setIcon({ path: path, }); } else { @@ -21,7 +21,7 @@ export default async function changeExtensionIcon(color?: ColorIconTypes): Promi theme === config.theme ? './graphics/wakatime-logo-38.png' : './graphics/wakatime-logo-38-white.png'; - await browser.browserAction.setIcon({ + await browser.action.setIcon({ path: path, }); } diff --git a/src/utils/changeExtensionTooltip.ts b/src/utils/changeExtensionTooltip.ts index ef4e4ba..9c9e647 100644 --- a/src/utils/changeExtensionTooltip.ts +++ b/src/utils/changeExtensionTooltip.ts @@ -12,5 +12,5 @@ export default async function changeExtensionTooltip(text: string): Promise Date: Thu, 19 Jan 2023 10:13:06 -0500 Subject: [PATCH 04/12] chore: replace plugin for user_agent in heartbeat payload --- src/core/WakaTimeCore.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index e2e2135..72921d2 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -213,13 +213,18 @@ class WakaTimeCore { * @private */ preparePayload(heartbeat: SendHeartbeat, type: string): Record { - return { + const payload: Record = { entity: heartbeat.url, - plugin: 'browser-wakatime/' + config.version, - project: heartbeat.project ?? '<>', time: moment().format('X'), type: type, + user_agent: 'browser-wakatime/' + config.version, }; + + if (heartbeat.project) { + payload.project = heartbeat.project; + } + + return payload; } /** From a0c3d35a958587d69de78256a753678dc6e3813e Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Mon, 23 Jan 2023 11:33:36 -0500 Subject: [PATCH 05/12] chore: changes axios for fetch, service workers dont go along with axios --- src/core/WakaTimeCore.ts | 29 ++++++++++++++++++++--------- src/utils/user.ts | 2 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 72921d2..1c6ab14 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -1,11 +1,10 @@ import axios, { AxiosResponse } from 'axios'; import moment from 'moment'; -import { Tabs } from 'webextension-polyfill'; +import browser, { Tabs } from 'webextension-polyfill'; import { AxiosUserResponse, User } from '../types/user'; import config from '../config/config'; import { SummariesPayload, GrandTotal } from '../types/summaries'; import changeExtensionState from '../utils/changeExtensionState'; -import inArray from '../utils/inArray'; import contains from '../utils/contains'; import { SendHeartbeat } from '../types/heartbeats'; import getDomainFromUrl from '../utils/getDomainFromUrl'; @@ -48,11 +47,23 @@ class WakaTimeCore { return userPayload.data.data; } + async getApiKey(): Promise { + const storage = await browser.storage.sync.get({ + apiKey: config.apiKey, + }); + const apiKey = storage.apiKey as string; + return apiKey; + } + /** * Depending on various factors detects the current active tab URL or domain, * and sends it to WakaTime for logging. */ - async recordHeartbeat(apiKey: string): Promise { + async recordHeartbeat(): Promise { + const apiKey = await this.getApiKey(); + if (!apiKey) { + return changeExtensionState('notLogging'); + } const items = await browser.storage.sync.get({ blacklist: '', loggingEnabled: config.loggingEnabled, @@ -234,14 +245,14 @@ class WakaTimeCore { * @param method * @returns {*} */ - async sendPostRequestToApi(payload: Record, api_key = '') { + async sendPostRequestToApi(payload: Record, apiKey = '') { try { - const response = await axios.post(config.heartbeatApiUrl, payload, { - params: { - api_key, - }, + const response = await fetch(`${config.heartbeatApiUrl}?api_key=${apiKey}`, { + body: JSON.stringify(payload), + method: 'POST', }); - return response.data; + const data = await response.json(); + return data; } catch (err: unknown) { await changeExtensionState('notSignedIn'); } diff --git a/src/utils/user.ts b/src/utils/user.ts index dd9e04e..e5c8c20 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -58,7 +58,7 @@ export const fetchUserData = async ( dispatch(setLoggingEnabled(items.loggingEnabled as boolean)); dispatch(setTotalTimeLoggedToday(totalTimeLoggedTodayResponse.text)); - await WakaTimeCore.recordHeartbeat(apiKey); + await WakaTimeCore.recordHeartbeat(); } catch (err: unknown) { await changeExtensionState('notSignedIn'); } From de4baec10ea4a44dcfb9d2fcdeb060c4d5fad810 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Mon, 23 Jan 2023 13:26:15 -0500 Subject: [PATCH 06/12] chore: setTitle and setIcon browser actions separate by browser --- src/utils/changeExtensionIcon.ts | 19 ++++++++++--------- src/utils/changeExtensionTooltip.ts | 7 ++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/utils/changeExtensionIcon.ts b/src/utils/changeExtensionIcon.ts index d1e8d1a..650634b 100644 --- a/src/utils/changeExtensionIcon.ts +++ b/src/utils/changeExtensionIcon.ts @@ -7,22 +7,23 @@ type ColorIconTypes = 'gray' | 'red' | 'white' | ''; * It changes the extension icon color. */ export default async function changeExtensionIcon(color?: ColorIconTypes): Promise { + let path; if (color) { - const path = `./graphics/wakatime-logo-38-${color}.png`; - - await browser.action.setIcon({ - path: path, - }); + path = `./graphics/wakatime-logo-38-${color}.png`; } else { const { theme } = await browser.storage.sync.get({ theme: config.theme, }); - const path = + path = theme === config.theme ? './graphics/wakatime-logo-38.png' : './graphics/wakatime-logo-38-white.png'; - await browser.action.setIcon({ - path: path, - }); + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (browser.browserAction) { + await browser.browserAction.setIcon({ path: path }); // Support for FF with manifest V2 + } else { + await browser.action.setIcon({ path: path }); // Support for Chrome with manifest V3 } } diff --git a/src/utils/changeExtensionTooltip.ts b/src/utils/changeExtensionTooltip.ts index 9c9e647..d6cc4ae 100644 --- a/src/utils/changeExtensionTooltip.ts +++ b/src/utils/changeExtensionTooltip.ts @@ -12,5 +12,10 @@ export default async function changeExtensionTooltip(text: string): Promise Date: Wed, 25 Jan 2023 12:23:39 -0500 Subject: [PATCH 07/12] chore: update user agent send to heartbeat api --- src/core/WakaTimeCore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 1c6ab14..9df3a8d 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -228,7 +228,7 @@ class WakaTimeCore { entity: heartbeat.url, time: moment().format('X'), type: type, - user_agent: 'browser-wakatime/' + config.version, + user_agent: `${navigator.userAgent} browser-wakatime/${config.version}`, }; if (heartbeat.project) { From f7da028ddc88fda3a03b843e2d3b5ebafa65f847 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Thu, 26 Jan 2023 14:22:48 -0500 Subject: [PATCH 08/12] chore: remove un used code --- src/background.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/background.ts b/src/background.ts index b441701..b2af2aa 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,18 +1,6 @@ import browser from 'webextension-polyfill'; import WakaTimeCore from './core/WakaTimeCore'; -// Add a listener to resolve alarms -browser.alarms.onAlarm.addListener(async (alarm) => { - // |alarm| can be undefined because onAlarm also gets called from - // window.setTimeout on old chrome versions. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (alarm?.name == 'heartbeatAlarm') { - console.log('recording a heartbeat - alarm triggered'); - - await WakaTimeCore.recordHeartbeat(); - } -}); - /** * Whenever a active tab is changed it records a heartbeat with that tab url. */ @@ -28,8 +16,6 @@ browser.windows.onFocusChanged.addListener(async (windowId) => { if (windowId != browser.windows.WINDOW_ID_NONE) { console.log('recording a heartbeat - active window changed'); await WakaTimeCore.recordHeartbeat(); - } else { - console.log('lost focus'); } }); From f0021cfa904d08ad8468ccbcfcc233040b7c395a Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Thu, 26 Jan 2023 15:56:26 -0500 Subject: [PATCH 09/12] chore: set browser name in userAgent heartbeat payload --- src/components/NavBar.test.tsx | 225 +++++++++++++++++++++++++++++++++ src/core/WakaTimeCore.ts | 6 +- src/manifests/firefox.json | 2 +- 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/components/NavBar.test.tsx diff --git a/src/components/NavBar.test.tsx b/src/components/NavBar.test.tsx new file mode 100644 index 0000000..1723959 --- /dev/null +++ b/src/components/NavBar.test.tsx @@ -0,0 +1,225 @@ +import React from 'react'; +import { renderWithProviders } from '../utils/test-utils'; +import NavBar from './NavBar'; + +jest.mock('webextension-polyfill', () => { + return { + runtime: { + getManifest: () => { + return { version: 'test-version' }; + }, + }, + }; +}); + +describe('NavBar', () => { + it('should render properly', () => { + const { container } = renderWithProviders(); + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); + // expect(container).toMatchInlineSnapshot(` + //
+ // + //
+ // `); + }); +}); diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 9df3a8d..dcf5274 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -224,11 +224,15 @@ class WakaTimeCore { * @private */ preparePayload(heartbeat: SendHeartbeat, type: string): Record { + let browserName = 'chrome'; + if (navigator.userAgent.includes('Firefox')) { + browserName = 'firefox'; + } const payload: Record = { entity: heartbeat.url, time: moment().format('X'), type: type, - user_agent: `${navigator.userAgent} browser-wakatime/${config.version}`, + user_agent: `${navigator.userAgent} ${browserName}-wakatime/${config.version}`, }; if (heartbeat.project) { diff --git a/src/manifests/firefox.json b/src/manifests/firefox.json index bb52660..96f77db 100644 --- a/src/manifests/firefox.json +++ b/src/manifests/firefox.json @@ -39,5 +39,5 @@ "storage", "idle" ], - "version": "2.0.1" + "version": "3.0.0" } From a4baf7cf8600bc9c637a568f5c12208987a8f8d4 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Fri, 27 Jan 2023 13:18:25 -0500 Subject: [PATCH 10/12] chore update user agent payload --- src/core/WakaTimeCore.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index dcf5274..1747436 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -225,14 +225,18 @@ class WakaTimeCore { */ preparePayload(heartbeat: SendHeartbeat, type: string): Record { let browserName = 'chrome'; + let userAgent; if (navigator.userAgent.includes('Firefox')) { browserName = 'firefox'; + userAgent = navigator.userAgent.match(/Firefox\/\S+/g)![0]; + } else { + userAgent = navigator.userAgent.match(/Chrome\/\S+/g)![0]; } const payload: Record = { entity: heartbeat.url, time: moment().format('X'), type: type, - user_agent: `${navigator.userAgent} ${browserName}-wakatime/${config.version}`, + user_agent: `${userAgent} ${browserName}-wakatime/${config.version}`, }; if (heartbeat.project) { From 3d3120ed4006d819f46b1807adc77dfa02028dcd Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Mon, 30 Jan 2023 11:39:43 -0500 Subject: [PATCH 11/12] chore: cache heartbeats request when they fail or users are offline --- .eslintrc.js | 1 + src/background.ts | 23 ++++++++++++++++++++++- src/core/WakaTimeCore.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index b3bf2d8..9f4ca73 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -37,6 +37,7 @@ module.exports = { }, plugins: ['react', '@typescript-eslint', 'typescript-sort-keys', 'sort-keys-fix'], rules: { + 'no-await-in-loop': 'off', 'prettier/prettier': 'error', 'sort-keys-fix/sort-keys-fix': 'error', 'testing-library/no-debug': 'off', diff --git a/src/background.ts b/src/background.ts index b2af2aa..6d43701 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,11 +1,32 @@ import browser from 'webextension-polyfill'; import WakaTimeCore from './core/WakaTimeCore'; +// Add a listener to resolve alarms +browser.alarms.onAlarm.addListener(async (alarm) => { + // |alarm| can be undefined because onAlarm also gets called from + // window.setTimeout on old chrome versions. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (alarm && alarm.name == 'heartbeatAlarm') { + // Checks if the user is online and if there are cached heartbeats requests, + // if so then procedd to send these payload to wakatime api + if (navigator.onLine) { + const { cachedHeartbeats } = await browser.storage.sync.get({ + cachedHeartbeats: [], + }); + await browser.storage.sync.set({ cachedHeartbeats: [] }); + await WakaTimeCore.sendCachedHeartbeatsRequest(cachedHeartbeats as Record[]); + } + } +}); + +// Create a new alarm for sending cached heartbeats. +browser.alarms.create('heartbeatAlarm', { periodInMinutes: 2 }); + /** * Whenever a active tab is changed it records a heartbeat with that tab url. */ browser.tabs.onActivated.addListener(async () => { - console.log('recording a heartbeat - active tab changed'); + console.log('recording a heartbeat - active tab changed '); await WakaTimeCore.recordHeartbeat(); }); diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 1747436..3a828a6 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -262,9 +262,44 @@ class WakaTimeCore { const data = await response.json(); return data; } catch (err: unknown) { + // Stores the payload of the request to be send later + const { cachedHeartbeats } = await browser.storage.sync.get({ + cachedHeartbeats: [], + }); + cachedHeartbeats.push(payload); + await browser.storage.sync.set({ cachedHeartbeats }); await changeExtensionState('notSignedIn'); } } + + /** + * Sends cached heartbeats request to wakatime api + * @param requests + */ + async sendCachedHeartbeatsRequest(requests: Record[]): Promise { + const apiKey = await this.getApiKey(); + if (!apiKey) { + return changeExtensionState('notLogging'); + } + const chunkSize = 50; // Create batches of max 50 request + for (let i = 0; i < requests.length; i += chunkSize) { + const chunk = requests.slice(i, i + chunkSize); + const requestsPromises: Promise[] = []; + chunk.forEach((request) => + requestsPromises.push( + fetch(`${config.heartbeatApiUrl}?api_key=${apiKey}`, { + body: JSON.stringify(request), + method: 'POST', + }), + ), + ); + try { + await Promise.all(requestsPromises); + } catch (error: unknown) { + console.log('Error sending heartbeats'); + } + } + } } export default new WakaTimeCore(); From 50abcfca9ec8fe8210724dc448d662a4ddbab4b0 Mon Sep 17 00:00:00 2001 From: Sebastian Velez Date: Mon, 30 Jan 2023 14:24:59 -0500 Subject: [PATCH 12/12] chore: remove un used code --- src/components/NavBar.test.tsx | 103 --------------------------------- 1 file changed, 103 deletions(-) diff --git a/src/components/NavBar.test.tsx b/src/components/NavBar.test.tsx index 1723959..0bb4348 100644 --- a/src/components/NavBar.test.tsx +++ b/src/components/NavBar.test.tsx @@ -118,108 +118,5 @@ describe('NavBar', () => { `); - // expect(container).toMatchInlineSnapshot(` - //
- // - //
- // `); }); });