Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
make build-chrome && \
Comment thread
RaoufGhrissi marked this conversation as resolved.
mkdir -p artifacts/chrome && \
unzip -o artifacts/chrome.zip -d artifacts/chrome

make build-firefox && \
mkdir -p artifacts/firefox && \
unzip -o artifacts/firefox.zip -d artifacts/firefox
110 changes: 83 additions & 27 deletions src/background/heartbeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,76 @@ import { getActiveWindowTab, getTab, getTabs } from './helpers'
import config from '../config'
import { AWClient, IEvent } from 'aw-client'
import { getBucketId, sendHeartbeat } from './client'
import { getEnabled, getHeartbeatData, setHeartbeatData } from '../storage'
import { getEnabled, getHeartbeatData, setHeartbeatData, getGmailEnabled } from '../storage'
import deepEqual from 'deep-equal'

export function setupMessageListener(client: AWClient) {
browser.runtime.onMessage.addListener(
async (message: any, sender: browser.Runtime.MessageSender) => {
const enabled = await getEnabled();
const gmailEnabled = await getGmailEnabled();
if (!enabled || !gmailEnabled) return;

if (message.type === 'AW_GMAIL_HEARTBEAT') {
const tab = sender.tab;
if (!tab || !tab.url || !tab.title) return;
if (!tab.url.includes('mail.google.com')) return;
const tabs = await getTabs();

const data: IEvent['data'] = {
url: tab.url,
title: tab.title,
audible: tab.audible ?? false,
incognito: tab.incognito,
tabCount: tabs.length,
...message.data,
};
await performHeartbeat(client, data);
}
},
)
}

async function performHeartbeat(
client: AWClient,
data: IEvent['data'],
options: { finalizeOnly?: boolean } = {}
) {
const bucketId = await getBucketId()
const now = new Date()
const previousData = await getHeartbeatData()
if (previousData && !deepEqual(previousData, data)) {
console.debug('[Background] Activity changed, finalizing previous session', previousData)
await sendHeartbeat(
client,
bucketId,
new Date(now.getTime() - 1),
previousData,
config.heartbeat.intervalInSeconds + 20,
).catch(() => {})
}

if (options.finalizeOnly) {
if (previousData) {
await browser.storage.local.remove('heartbeatData');
}
return;
}
Comment thread
RaoufGhrissi marked this conversation as resolved.

console.debug('[Background] Sending heartbeat', data)
await sendHeartbeat(
client,
bucketId,
now,
data,
config.heartbeat.intervalInSeconds + 20,
).catch((err: unknown) => {
console.error('[Background] Failed to send heartbeat:', err);
})

await setHeartbeatData(data)
}

async function heartbeat(
client: AWClient,
tab: browser.Tabs.Tab | undefined,
Expand All @@ -27,34 +94,23 @@ async function heartbeat(
return
}

const now = new Date()
const data: IEvent['data'] = {
url: tab.url,
title: tab.title,
audible: tab.audible ?? false,
incognito: tab.incognito,
tabCount: tabCount,
}
const previousData = await getHeartbeatData()
if (previousData && !deepEqual(previousData, data)) {
console.debug('Sending heartbeat for previous data', previousData)
await sendHeartbeat(
client,
await getBucketId(),
new Date(now.getTime() - 1),
previousData,
config.heartbeat.intervalInSeconds + 20,
)

const gmailEnabled = await getGmailEnabled();
if (gmailEnabled && tab.url.includes('mail.google.com')) {
// Sharp cut: finalize the previous activity (e.g. if we came from Google Search)
// but don't start the 'Generic' Gmail event. Gmail.ts will do that with metadata.
await performHeartbeat(client, data, { finalizeOnly: true });
return;
}
console.debug('Sending heartbeat', data)
await sendHeartbeat(
client,
await getBucketId(),
now,
data,
config.heartbeat.intervalInSeconds + 20,
)
await setHeartbeatData(data)

await performHeartbeat(client, data);
}

export const sendInitialHeartbeat = async (client: AWClient) => {
Expand All @@ -76,9 +132,9 @@ export const heartbeatAlarmListener =

export const tabActivatedListener =
(client: AWClient) =>
async (activeInfo: browser.Tabs.OnActivatedActiveInfoType) => {
const tab = await getTab(activeInfo.tabId)
const tabs = await getTabs()
console.debug('Sending heartbeat for tab activation', tab)
await heartbeat(client, tab, tabs.length)
}
async (activeInfo: browser.Tabs.OnActivatedActiveInfoType) => {
const tab = await getTab(activeInfo.tabId)
const tabs = await getTabs()
console.debug('Sending heartbeat for tab activation', tab)
await heartbeat(client, tab, tabs.length)
}
10 changes: 8 additions & 2 deletions src/background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
heartbeatAlarmListener,
sendInitialHeartbeat,
tabActivatedListener,
setupMessageListener,
} from './heartbeat'
import { getClient, detectHostname } from './client'
import {
Expand All @@ -15,6 +16,7 @@ import {
setHostname,
waitForEnabled,
} from '../storage'
import { AWClient } from 'aw-client'

async function getIsConsentRequired() {
if (!config.requireConsent) return false
Expand All @@ -24,7 +26,7 @@ async function getIsConsentRequired() {
.catch(() => true)
}

async function autodetectHostname() {
async function autodetectHostname(client: AWClient) {
const hostname = await getHostname()
if (hostname === undefined) {
const detectedHostname = await detectHostname(client)
Expand Down Expand Up @@ -57,13 +59,17 @@ browser.runtime.onInstalled.addListener(async () => {
})
}

await autodetectHostname()
await autodetectHostname(client)
})

console.debug('Creating alarms and tab listeners')
browser.alarms.create(config.heartbeat.alarmName, {
periodInMinutes: Math.floor(config.heartbeat.intervalInSeconds / 60),
})

// Set up Gmail message listener (other watchers will be added later)
setupMessageListener(client)

browser.alarms.onAlarm.addListener(heartbeatAlarmListener(client))
browser.tabs.onActivated.addListener(tabActivatedListener(client))

Expand Down
158 changes: 158 additions & 0 deletions src/content/gmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import browser from 'webextension-polyfill'
import deepEqual from 'deep-equal'
import config from '../config'

let lastData: any | null = null;

function isExtensionValid() {
return typeof browser !== 'undefined' && !!browser.storage && !!browser.runtime?.id;
}


function getComposeMetadata(form: HTMLElement) {
const getRecipients = (name: string) =>
Array.from(
form.querySelectorAll(`div[name="${name}"] [data-hovercard-id]`),
).map((el) => el.getAttribute('data-hovercard-id'))
.filter(Boolean) as string[];

return {
gmail_activity: 'composing_email',
subject: (form.querySelector('input[name="subjectbox"]') as HTMLInputElement)?.value || '',
to: getRecipients('to'),
cc: getRecipients('cc'),
bcc: getRecipients('bcc'),
};
}

function sendGmailHeartbeat() {
if (!isExtensionValid()) {
// Don't kill tracking — just skip this tick.
// The onChanged listener will handle re-evaluation.
return;
}
if (document.visibilityState === 'hidden') {
return;
}

const hash = window.location.hash;
// for simplity in MVP:
// - if many emails forms are open, we only track the first one
const form = document.querySelector('div[role="dialog"] form') as HTMLElement | null;

let activity = 'reading_inbox';
let meta: any = { gmail_activity: activity };

if (form) {
activity = 'composing_email';
meta = getComposeMetadata(form);
} else if (
hash.includes('inbox/') ||
hash.includes('sent/') ||
hash.includes('all/')
) {
/**
* NOTE on Fragility: The selectors below (span.gD, .gE, h2.hP) are internal
* Gmail class names. These are not part of a stable API and may change
* during Gmail frontend updates. High-fidelity tracking may require
* maintenance if these selectors break.
*/
const fromEl = document.querySelector('span.gD');
const from =
fromEl?.getAttribute('email') ||
fromEl?.getAttribute('data-hovercard-id') ||
(fromEl as HTMLElement)?.innerText ||
'';
const to = Array.from(
document.querySelectorAll('.gE [email], .gE [data-hovercard-id]'),
)
.map(
(el) => el.getAttribute('email') || el.getAttribute('data-hovercard-id'),
)
.filter((e) => e && e !== from) as string[];

activity = 'reading_email';
meta = {
gmail_activity: activity,
subject: (document.querySelector('h2.hP') as HTMLElement)?.innerText || '',
from,
to,
};
}

if (!deepEqual(lastData, meta)) {
lastData = meta;
browser.runtime.sendMessage({
type: 'AW_GMAIL_HEARTBEAT',
data: meta
}).catch(() => {})
}
}

let detectIntervalId: ReturnType<typeof setInterval> | null = null;
let pulseIntervalId: ReturnType<typeof setInterval> | null = null;

function startTracking() {
if (detectIntervalId !== null) {
return;
}

detectIntervalId = setInterval(sendGmailHeartbeat, 5000);
pulseIntervalId = setInterval(() => {
if (!isExtensionValid()) {
return;
}
if (lastData && document.visibilityState === 'visible') {
try {
browser.runtime.sendMessage({
type: 'AW_GMAIL_HEARTBEAT',
data: lastData
}).catch(() => {})
} catch (err) {
// Extension context invalidated
}
}
}, config.heartbeat.intervalInSeconds * 1000);

sendGmailHeartbeat();
}

async function refreshTracking() {
if (!isExtensionValid()) {
return;
}
try {
const settings = await browser.storage.local.get(['gmailEnabled', 'enabled']);
const shouldTrack = Boolean(settings.gmailEnabled && settings.enabled);

if (shouldTrack) {
startTracking();
} else {
stopTracking();
}
} catch (err) {
console.error('[Gmail Content] Failed to refresh tracking state', err);
}
}

function stopTracking() {
if (detectIntervalId !== null) {
clearInterval(detectIntervalId)
detectIntervalId = null;
}
if (pulseIntervalId !== null) {
clearInterval(pulseIntervalId)
pulseIntervalId = null;
}
lastData = null;
}

browser.storage.local.get(['gmailEnabled', 'enabled']).then(() => {
refreshTracking();
})

browser.storage.onChanged.addListener((changes) => {
if ('gmailEnabled' in changes || 'enabled' in changes) {
refreshTracking();
}
})
12 changes: 11 additions & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,15 @@
"gecko": {
"id": "{ef87d84c-2127-493f-b952-5b4e744245bc}"
}
}
},
"content_scripts": [
{
"matches": [
"*://mail.google.com/*"
],
"js": [
"src/content/gmail.ts"
]
}
]
}
9 changes: 9 additions & 0 deletions src/popup/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
</td>
</tr>

<tr>
<!-- TODO: we should move this to another page -->
<!-- if the number of email providers or websites that we want to extract data from increases -->
<th align="right">Gmail details:</th>
<td>
<input type="checkbox" id="status-gmail-enabled-checkbox" />
</td>
</tr>

<tr>
<th align="right">Connected:</th>
<td id="status-connected-icon">
Expand Down
Loading