Skip to content

Commit 9a9eb2d

Browse files
committed
implement telemetry opt-in/out via token bridge
2 parents 561684f + 7c4066b commit 9a9eb2d

5 files changed

Lines changed: 229 additions & 24 deletions

File tree

packages/addon/public/token-bridge.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ const OPEN_MANAGEMENT_PAGE = 'OPEN_MANAGEMENT_PAGE';
1818
// PENDING_ADDON_TOKEN_RESPONSE : background → bridge → web page ("here it is")
1919
const GET_PENDING_ADDON_TOKEN = 'TB/GET_PENDING_ADDON_TOKEN';
2020
const PENDING_ADDON_TOKEN_RESPONSE = 'TB/PENDING_ADDON_TOKEN_RESPONSE';
21+
// Telemetry pref bridge (issue #952). The hosted Send dashboard cannot call the
22+
// browser.Telemetry experiment API, so it asks the background for the pref:
23+
// GET_TELEMETRY_STATE : web page → bridge → background
24+
// TELEMETRY_STATE_RESPONSE : background → bridge → web page
25+
// TELEMETRY_STATE_CHANGED : background → bridge → web page (runtime change)
26+
const GET_TELEMETRY_STATE = 'TB/GET_TELEMETRY_STATE';
27+
const TELEMETRY_STATE_RESPONSE = 'TB/TELEMETRY_STATE_RESPONSE';
28+
const TELEMETRY_STATE_CHANGED = 'TB/TELEMETRY_STATE_CHANGED';
2129

2230
window.postMessage({ type: BRIDGE_READY }, window.location.origin);
2331
console.log(`[🌉 token-bridge] the token bridge has loaded.`);
@@ -136,6 +144,13 @@ window.addEventListener('message', (e) => {
136144
type: GET_PENDING_ADDON_TOKEN,
137145
});
138146
}
147+
148+
// ----- Web to add-on: hosted dashboard asks for the telemetry pref -----
149+
if (e?.data?.type === GET_TELEMETRY_STATE) {
150+
browser.runtime.sendMessage({
151+
type: GET_TELEMETRY_STATE,
152+
});
153+
}
139154
});
140155

141156
// Listen for responses from background script and forward to web app
@@ -179,4 +194,18 @@ browser.runtime.onMessage.addListener((message) => {
179194
window.location.origin
180195
);
181196
}
197+
198+
// ----- Add-on to Web: telemetry pref value / runtime change (issue #952) -----
199+
if (
200+
message.type === TELEMETRY_STATE_RESPONSE ||
201+
message.type === TELEMETRY_STATE_CHANGED
202+
) {
203+
window.postMessage(
204+
{
205+
type: message.type,
206+
enabled: message.enabled,
207+
},
208+
window.location.origin
209+
);
210+
}
182211
});

packages/addon/src/background.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
FORCE_CLOSE_WINDOW,
1919
GET_LOGIN_STATE,
2020
GET_PENDING_ADDON_TOKEN,
21+
GET_TELEMETRY_STATE,
2122
LOGIN_STATE_RESPONSE,
2223
OIDC_TOKEN,
2324
OIDC_USER,
@@ -31,6 +32,8 @@ import {
3132
SIGN_IN_COMPLETE,
3233
SIGN_OUT,
3334
STORAGE_KEY_AUTH,
35+
TELEMETRY_STATE_CHANGED,
36+
TELEMETRY_STATE_RESPONSE,
3437
} from '@send-frontend/lib/const';
3538

3639
import init from '@send-frontend/lib/init';
@@ -447,6 +450,28 @@ browser.runtime.onMessage.addListener(async (message, sender) => {
447450
break;
448451
}
449452

453+
case GET_TELEMETRY_STATE: {
454+
// The hosted Send dashboard (send.js) runs as a web page and cannot call
455+
// the browser.Telemetry experiment API directly, so it requests the
456+
// Thunderbird telemetry pref here via the token-bridge (issue #952).
457+
// getUploadEnabled() already fails closed to false on error.
458+
let enabled = false;
459+
try {
460+
enabled = await browser.Telemetry.getUploadEnabled();
461+
} catch (e) {
462+
console.warn('[onMessage] Failed to read telemetry pref:', e);
463+
}
464+
if (sender?.tab?.id) {
465+
browser.tabs
466+
.sendMessage(sender.tab.id, {
467+
type: TELEMETRY_STATE_RESPONSE,
468+
enabled,
469+
})
470+
.catch(() => {});
471+
}
472+
break;
473+
}
474+
450475
// ----- Add-On to Web — Step 6: Read staged token and respond to tab -----
451476
// The /addon-auth page (running as a normal web tab) cannot call
452477
// browser.storage.local directly. It sends GET_PENDING_ADDON_TOKEN via
@@ -765,6 +790,30 @@ function initAccountHubListener() {
765790
});
766791
}
767792

793+
// ==============================================
794+
// Broadcast Thunderbird telemetry pref changes to hosted Send pages (issue
795+
// #952). The dashboard (send.js) runs as a web page without the
796+
// browser.Telemetry experiment API, so it can't observe the pref itself. We
797+
// watch it here and push changes through the token-bridge so telemetry can
798+
// start/stop at runtime without a reload.
799+
function initTelemetryListener() {
800+
browser.Telemetry.onChanged.addListener(async (enabled) => {
801+
const tabs = await browser.tabs.query({});
802+
tabs.forEach((tab) => {
803+
if (tab.id) {
804+
browser.tabs
805+
.sendMessage(tab.id, {
806+
type: TELEMETRY_STATE_CHANGED,
807+
enabled,
808+
})
809+
.catch(() => {
810+
// Ignore tabs that can't receive messages.
811+
});
812+
}
813+
});
814+
});
815+
}
816+
768817
(async function main() {
769818
await checkAndUninstallIfDeprecated();
770819
initMenu();
@@ -790,6 +839,7 @@ function initAccountHubListener() {
790839
}
791840
initStorageWatcher();
792841
initAccountHubListener();
842+
initTelemetryListener();
793843
})().catch((error) => {
794844
console.error('Error initializing background.js', error);
795845
});

packages/send/frontend/src/lib/const.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,14 @@ export const STORAGE_KEY_AUTH = 'STORAGE_KEY_AUTH';
5555
export const PENDING_ADDON_TOKEN = 'tbpro-pending-addon-token';
5656
export const GET_PENDING_ADDON_TOKEN = 'TB/GET_PENDING_ADDON_TOKEN';
5757
export const PENDING_ADDON_TOKEN_RESPONSE = 'TB/PENDING_ADDON_TOKEN_RESPONSE';
58+
59+
// Telemetry pref bridge (issue #952). The hosted Send dashboard (send.js) runs
60+
// as a web page without access to the `browser.Telemetry` experiment API, so it
61+
// asks the background script for the Thunderbird telemetry pref via the
62+
// token-bridge instead of reading it directly.
63+
// GET_TELEMETRY_STATE : web page → bridge → background ("what's the pref?")
64+
// TELEMETRY_STATE_RESPONSE : background → bridge → web page ("{ enabled }")
65+
// TELEMETRY_STATE_CHANGED : background → bridge → web page (runtime pref change)
66+
export const GET_TELEMETRY_STATE = 'TB/GET_TELEMETRY_STATE';
67+
export const TELEMETRY_STATE_RESPONSE = 'TB/TELEMETRY_STATE_RESPONSE';
68+
export const TELEMETRY_STATE_CHANGED = 'TB/TELEMETRY_STATE_CHANGED';

packages/send/frontend/src/lib/telemetryConsent.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
import {
3+
GET_TELEMETRY_STATE,
4+
TELEMETRY_STATE_RESPONSE,
5+
} from './const';
26
import { isTelemetryAllowed } from './telemetryConsent';
37

48
function setUserAgent(ua: string) {
@@ -8,6 +12,23 @@ function setUserAgent(ua: string) {
812
});
913
}
1014

15+
/**
16+
* Simulates the token-bridge content script: when the page asks for the
17+
* telemetry state, reply with the given value. Returns an uninstall function.
18+
*/
19+
function installFakeBridge(enabled: boolean) {
20+
const bridge = (event: MessageEvent) => {
21+
if (event.data?.type === GET_TELEMETRY_STATE) {
22+
window.postMessage(
23+
{ type: TELEMETRY_STATE_RESPONSE, enabled },
24+
window.location.origin
25+
);
26+
}
27+
};
28+
window.addEventListener('message', bridge);
29+
return () => window.removeEventListener('message', bridge);
30+
}
31+
1132
const THUNDERBIRD_UA =
1233
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:140.0) Gecko/20100101 Thunderbird/140.0';
1334
const WEB_UA =
@@ -41,10 +62,38 @@ describe('isTelemetryAllowed', () => {
4162
await expect(isTelemetryAllowed()).resolves.toBe(false);
4263
});
4364

44-
it('fails closed inside Thunderbird when the experiment API is unavailable', async () => {
65+
it('honors the pref via the token-bridge when the API is absent (dashboard) — enabled', async () => {
4566
setUserAgent(THUNDERBIRD_UA);
46-
// No `browser.Telemetry` available.
47-
await expect(isTelemetryAllowed()).resolves.toBe(false);
67+
// No `browser.Telemetry` (hosted dashboard context), but the bridge answers.
68+
const uninstall = installFakeBridge(true);
69+
try {
70+
await expect(isTelemetryAllowed()).resolves.toBe(true);
71+
} finally {
72+
uninstall();
73+
}
74+
});
75+
76+
it('honors the pref via the token-bridge when the API is absent (dashboard) — disabled', async () => {
77+
setUserAgent(THUNDERBIRD_UA);
78+
const uninstall = installFakeBridge(false);
79+
try {
80+
await expect(isTelemetryAllowed()).resolves.toBe(false);
81+
} finally {
82+
uninstall();
83+
}
84+
});
85+
86+
it('fails closed inside Thunderbird when neither the API nor the bridge answers', async () => {
87+
setUserAgent(THUNDERBIRD_UA);
88+
// No `browser.Telemetry` and no bridge listening: the request times out.
89+
vi.useFakeTimers();
90+
try {
91+
const pending = isTelemetryAllowed();
92+
await vi.advanceTimersByTimeAsync(2000);
93+
await expect(pending).resolves.toBe(false);
94+
} finally {
95+
vi.useRealTimers();
96+
}
4897
});
4998

5099
it('fails closed inside Thunderbird when reading the pref throws', async () => {

packages/send/frontend/src/lib/telemetryConsent.ts

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,30 @@
88
* - Outside Thunderbird (public website): telemetry behaves as before (allowed).
99
* - Inside Thunderbird, on an add-on (moz-extension) page: read the pref via the
1010
* `browser.Telemetry` experiment API.
11-
* - Inside Thunderbird but the experiment API is unavailable / errors: fail
12-
* closed (no telemetry).
11+
* - Inside Thunderbird on the hosted Send dashboard (send.js runs as a web page
12+
* with no experiment API): ask the background script for the pref over the
13+
* token-bridge (issue #952).
14+
* - Inside Thunderbird but neither the experiment API nor the bridge answers:
15+
* fail closed (no telemetry).
1316
*
1417
* The `browser.Telemetry` namespace is provided by the add-on's experiment API
1518
* (packages/addon/public/api/Telemetry). The frontend types come from
1619
* @types/firefox-webext-browser, which does not know about custom experiment
1720
* APIs, hence the `@ts-ignore` accesses below (matching extension-store.ts).
1821
*/
1922

23+
import {
24+
GET_TELEMETRY_STATE,
25+
TELEMETRY_STATE_CHANGED,
26+
TELEMETRY_STATE_RESPONSE,
27+
} from './const';
28+
29+
// How long to wait for the token-bridge to answer before failing closed. The
30+
// bridge content script is injected at document_start, so it is normally
31+
// present before this runs; the timeout covers the case where it is not (e.g.
32+
// the add-on isn't installed).
33+
const BRIDGE_TIMEOUT_MS = 2000;
34+
2035
const isInsideThunderbird = (): boolean =>
2136
typeof navigator !== 'undefined' &&
2237
navigator.userAgent.includes('Thunderbird');
@@ -35,6 +50,41 @@ const getTelemetryApi = (): any => {
3550
return undefined;
3651
};
3752

53+
/**
54+
* Requests the Thunderbird telemetry pref from the background script via the
55+
* token-bridge, for contexts (the hosted Send dashboard) that lack the
56+
* `browser.Telemetry` experiment API. Resolves false (fail closed) if the
57+
* bridge does not answer within the timeout.
58+
*/
59+
function requestTelemetryStateViaBridge(): Promise<boolean> {
60+
if (typeof window === 'undefined') {
61+
return Promise.resolve(false);
62+
}
63+
64+
return new Promise((resolve) => {
65+
const cleanup = () => {
66+
clearTimeout(timer);
67+
window.removeEventListener('message', handler);
68+
};
69+
70+
const timer = setTimeout(() => {
71+
// Bridge absent or unresponsive — fail closed.
72+
cleanup();
73+
resolve(false);
74+
}, BRIDGE_TIMEOUT_MS);
75+
76+
const handler = (event: MessageEvent) => {
77+
if (event.data?.type === TELEMETRY_STATE_RESPONSE) {
78+
cleanup();
79+
resolve(Boolean(event.data.enabled));
80+
}
81+
};
82+
83+
window.addEventListener('message', handler);
84+
window.postMessage({ type: GET_TELEMETRY_STATE }, window.location.origin);
85+
});
86+
}
87+
3888
/**
3989
* Resolves whether telemetry (Sentry + PostHog) is allowed to run.
4090
* Fails closed (false) when inside Thunderbird and the pref cannot be read.
@@ -46,16 +96,18 @@ export async function isTelemetryAllowed(): Promise<boolean> {
4696
}
4797

4898
const api = getTelemetryApi();
49-
if (!api?.getUploadEnabled) {
50-
// Inside Thunderbird but the experiment API is unavailable: fail closed.
51-
return false;
99+
if (api?.getUploadEnabled) {
100+
// moz-extension add-on page: read the pref directly.
101+
try {
102+
return Boolean(await api.getUploadEnabled());
103+
} catch {
104+
return false;
105+
}
52106
}
53107

54-
try {
55-
return Boolean(await api.getUploadEnabled());
56-
} catch {
57-
return false;
58-
}
108+
// Inside Thunderbird without the direct experiment API (the hosted Send
109+
// dashboard runs as a web page): ask the background via the token-bridge.
110+
return requestTelemetryStateViaBridge();
59111
}
60112

61113
/**
@@ -67,18 +119,32 @@ export function onTelemetryChanged(
67119
cb: (enabled: boolean) => void
68120
): () => void {
69121
const api = getTelemetryApi();
70-
if (!api?.onChanged?.addListener) {
71-
return () => {};
122+
if (api?.onChanged?.addListener) {
123+
// moz-extension add-on page: observe the pref directly.
124+
const listener = (enabled: boolean) => cb(Boolean(enabled));
125+
api.onChanged.addListener(listener);
126+
127+
return () => {
128+
try {
129+
api.onChanged.removeListener?.(listener);
130+
} catch {
131+
// ignore
132+
}
133+
};
72134
}
73135

74-
const listener = (enabled: boolean) => cb(Boolean(enabled));
75-
api.onChanged.addListener(listener);
136+
// Inside Thunderbird without the direct API (the hosted Send dashboard): the
137+
// background pushes pref changes through the token-bridge as window messages.
138+
if (isInsideThunderbird() && typeof window !== 'undefined') {
139+
const handler = (event: MessageEvent) => {
140+
if (event.data?.type === TELEMETRY_STATE_CHANGED) {
141+
cb(Boolean(event.data.enabled));
142+
}
143+
};
144+
window.addEventListener('message', handler);
145+
return () => window.removeEventListener('message', handler);
146+
}
76147

77-
return () => {
78-
try {
79-
api.onChanged.removeListener?.(listener);
80-
} catch {
81-
// ignore
82-
}
83-
};
148+
// Public website (outside Thunderbird): nothing to observe.
149+
return () => {};
84150
}

0 commit comments

Comments
 (0)