tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 81399e64aeaee7a8dae90c9d44030162fc4d60c1
parent 7dd58898313f5f03e6f54f583607973469363329
Author: Kagami Sascha Rosylight <krosylight@proton.me>
Date:   Wed,  3 Dec 2025 18:24:15 +0000

Bug 1969357 - Add telemetry for DWP payloads r=asuth,dom-worker-reviewers

Also extend the data clear telemetry, which is also relevant for DWP as one of the arguments was that DWP subscription should not be cleared with other data as long as the permission exists.

Differential Revision: https://phabricator.services.mozilla.com/D274428

Diffstat:
Mdom/push/metrics.yaml | 31+++++++++++++++++++++++++++++--
Mdom/serviceworkers/ServiceWorkerEvents.cpp | 58+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mdom/serviceworkers/ServiceWorkerEvents.h | 1+
Atesting/web-platform/mozilla/tests/push-api/declarative-glean.https.any.js | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/mozilla/tests/push-api/push-sw.js | 5+++++
Atesting/web-platform/mozilla/tests/resources/GleanTest.js | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/mozilla/tests/websockets/handlers/mozilla_push_dummy_wsh.py | 3+++
7 files changed, 234 insertions(+), 3 deletions(-)

diff --git a/dom/push/metrics.yaml b/dom/push/metrics.yaml @@ -23,6 +23,7 @@ web_push: notification_emails: - krosylight@mozilla.com expires: never + error_code: type: labeled_counter description: > @@ -40,6 +41,7 @@ web_push: notification_emails: - krosylight@mozilla.com expires: never + content_encoding: type: labeled_counter description: > @@ -54,6 +56,7 @@ web_push: notification_emails: - krosylight@mozilla.com expires: never + unsubscribed_by_clearing_data: type: counter description: > @@ -64,13 +67,13 @@ web_push: - https://bugzilla.mozilla.org/show_bug.cgi?id=1948721 notification_emails: - krosylight@mozilla.com - expires: 149 + expires: 159 api_notify: type: counter description: > Number of push messages that were successfully decrypted and delivered to - a ServiceWorker. + a ServiceWorker when required. This metric was generated to correspond to the Legacy Telemetry count histogram PUSH_API_NOTIFY. @@ -82,3 +85,27 @@ web_push: - push@mozilla.com expires: never telemetry_mirror: h#PUSH_API_NOTIFY + + declarative: + type: counter + description: > + Number of push messages in the Declarative Web Push format. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1969357 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1969357 + notification_emails: + - krosylight@mozilla.com + expires: never + + declarative_mutable: + type: counter + description: > + Number of push messages in the Declarative Web Push format with mutable: true. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1969357 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1969357 + notification_emails: + - krosylight@mozilla.com + expires: never diff --git a/dom/serviceworkers/ServiceWorkerEvents.cpp b/dom/serviceworkers/ServiceWorkerEvents.cpp @@ -12,6 +12,7 @@ #include "ServiceWorkerManager.h" #include "js/Conversions.h" #include "js/Exception.h" // JS::ExceptionStack, JS::StealPendingExceptionStack +#include "js/RootingAPI.h" #include "js/TypeDecls.h" #include "mozilla/Encoding.h" #include "mozilla/ErrorResult.h" @@ -32,6 +33,7 @@ #include "mozilla/dom/TypedArray.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerScope.h" +#include "mozilla/glean/DomPushMetrics.h" #include "mozilla/net/NeckoChannelParams.h" #include "nsComponentManagerUtils.h" #include "nsContentPolicyUtils.h" @@ -1023,7 +1025,12 @@ nsresult ExtractBytesFromData( PushMessageData::PushMessageData(nsIGlobalObject* aOwner, nsTArray<uint8_t>&& aBytes) - : mOwner(aOwner), mBytes(std::move(aBytes)) {} + : mOwner(aOwner), mBytes(std::move(aBytes)) { + AutoJSAPI jsapi; + if (jsapi.Init(mOwner)) { + SetUseCounterIfDeclarative(jsapi.cx()); + } +} PushMessageData::~PushMessageData() = default; @@ -1114,6 +1121,55 @@ uint8_t* PushMessageData::GetContentsCopy() { return reinterpret_cast<uint8_t*>(data); } +// This partially implements the parsing algorithm for a simple detection: +// https://w3c.github.io/push-api/#declarative-push-message +void PushMessageData::SetUseCounterIfDeclarative(JSContext* aCx) { + // NOTE(krosylight): This could be in the parent process but: + // 1. The desktop and Android implementations use different modules for push. + // The common path starts with PushNotifier which is not a great place for + // this use counter either. + // We'll need to reconsider this as we don't want to ping content processes + // at all for non-mutable DWP messages when we add the support. + // 2. The decode would happen twice; this way it happens once with the cache. + + // Step 1: Let message be the result of parsing JSON bytes to an Infra value + // given bytes. If that throws an exception, then return failure. + // Step 2: If message is not a map, then return failure. + JS::Rooted<JS::Value> message(aCx); + IgnoredErrorResult rv; + Json(aCx, &message, rv); + if (rv.Failed() || !message.isObject()) { + return; + } + + // Step 3: If message["web_push"] does not exist or is not 8030, then return + // failure. + JS::Rooted<JSObject*> messageObject(aCx, message.toObjectOrNull()); + JS::Rooted<JS::Value> property(aCx); + if (!JS_GetProperty(aCx, messageObject, "web_push", &property)) { + rv.StealExceptionFromJSContext(aCx); + return; + } + if (!property.isNumber() || property.toNumber() != 8030) { + return; + } + + glean::web_push::declarative.Add(); + + // Step 30: If message["mutable"] exists and message["mutable"] is a boolean, + // then set mutable to message["mutable"]. + // (But we just detect whether it's true or not for now) + if (!JS_GetProperty(aCx, messageObject, "mutable", &property)) { + rv.StealExceptionFromJSContext(aCx); + return; + } + if (!property.isBoolean() || !property.toBoolean()) { + return; + } + + glean::web_push::declarative_mutable.Add(); +} + PushEvent::PushEvent(EventTarget* aOwner) : ExtendableEvent(aOwner) {} already_AddRefed<PushEvent> PushEvent::Constructor( diff --git a/dom/serviceworkers/ServiceWorkerEvents.h b/dom/serviceworkers/ServiceWorkerEvents.h @@ -223,6 +223,7 @@ class PushMessageData final : public nsISupports, public nsWrapperCache { nsresult EnsureDecodedText(); uint8_t* GetContentsCopy(); + void SetUseCounterIfDeclarative(JSContext* aCx); }; class PushEvent final : public ExtendableEvent { diff --git a/testing/web-platform/mozilla/tests/push-api/declarative-glean.https.any.js b/testing/web-platform/mozilla/tests/push-api/declarative-glean.https.any.js @@ -0,0 +1,80 @@ +// META: global=window-module +// META: script=/_mozilla/resources/GleanTest.js +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/notifications/resources/helpers.js + +import { encrypt } from "/push-api/resources/helpers.js" + +let registration; +let subscription; + +promise_setup(async (t) => { + await trySettingPermission("granted"); + registration = await prepareActiveServiceWorker("push-sw.js"); + subscription = await registration.pushManager.subscribe(); +}); + +async function pushAndReceiveMessage(t, message) { + await GleanTest.testResetFOG(); + + const result = await encrypt( + new TextEncoder().encode(message), + subscription.getKey("p256dh"), + subscription.getKey("auth") + ); + + const { promise, resolve } = Promise.withResolvers(); + const controller = new AbortController(); + navigator.serviceWorker.addEventListener("message", ev => { + if (ev.data.data !== message) { + return; + } + controller.abort(); + resolve(); + }, { signal: controller.signal }); + + await fetch(subscription.endpoint, { + method: "post", + ...result + }); + + await promise; + await GleanTest.flush(); +} + +promise_test(async (t) => { + await pushAndReceiveMessage(t, "hello"); + + const notify = await GleanTest.webPush.apiNotify.testGetValue(); + const dwp = await GleanTest.webPush.declarative.testGetValue(); + const mutable = await GleanTest.webPush.declarativeMutable.testGetValue(); + + assert_equals(notify, 1, "notify should always increment for valid push messages"); + assert_equals(dwp, null, "declarative should not increment on non-DWP"); + assert_equals(mutable, null, "declarativeMutable should not increment on non-DWP"); +}, "Non-declarative web push"); + +promise_test(async (t) => { + await pushAndReceiveMessage(t, `{ "web_push": 8030 }`); + + const notify = await GleanTest.webPush.apiNotify.testGetValue(); + const dwp = await GleanTest.webPush.declarative.testGetValue(); + const mutable = await GleanTest.webPush.declarativeMutable.testGetValue(); + + assert_equals(notify, 1, "notify should always increment for valid push messages"); + assert_equals(dwp, 1, "declarative should increment on DWP"); + assert_equals(mutable, null, "declarativeMutable should increment on DWP"); +}, "Declarative web push"); + +promise_test(async (t) => { + await pushAndReceiveMessage(t, `{ "web_push": 8030, "mutable": true }`); + + const notify = await GleanTest.webPush.apiNotify.testGetValue(); + const dwp = await GleanTest.webPush.declarative.testGetValue(); + const mutable = await GleanTest.webPush.declarativeMutable.testGetValue(); + + assert_equals(notify, 1, "notify should always increment for valid push messages"); + assert_equals(dwp, 1, "declarative should increment on mutable DWP"); + assert_equals(mutable, 1, "declarativeMutable should increment on mutable DWP"); +}, "Declarative web push with mutable: true"); diff --git a/testing/web-platform/mozilla/tests/push-api/push-sw.js b/testing/web-platform/mozilla/tests/push-api/push-sw.js @@ -5,6 +5,11 @@ async function postAll(data) { } } +onpush = ev => postAll({ + type: ev.type, + data: ev.data.text(), +}); + onpushsubscriptionchange = ev => { postAll({ type: ev.type, diff --git a/testing/web-platform/mozilla/tests/resources/GleanTest.js b/testing/web-platform/mozilla/tests/resources/GleanTest.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// This file is a copy of testing/mochitest/tests/SimpleTest/GleanTest.js, +// as WPT does not serve SimpleTest. + +var GleanTest; +(function () { + async function testResetFOG() { + return SpecialPowers.spawnChrome([], async () => { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + }); + } + + async function flush() { + return SpecialPowers.spawnChrome([], async () => { + await Services.fog.testFlushAllChildren(); + }); + } + + async function testGetValue(chain) { + return SpecialPowers.spawnChrome([chain], async chain => { + await Services.fog.testFlushAllChildren(); + const window = this.browsingContext.topChromeWindow; + let glean = window.Glean; + while (chain.length) { + glean = glean[chain.shift()]; + } + return glean.testGetValue(); + }); + } + + function recurse(chain = []) { + return new Proxy( + {}, + { + get(_, prop) { + if (chain.length === 0) { + if (prop === "testResetFOG") { + return testResetFOG; + } else if (prop === "flush") { + return flush; + } + } + if (chain.length >= 2 && prop === "testGetValue") { + return () => testGetValue(chain); + } + return recurse(chain.concat(prop)); + }, + } + ); + } + + window.GleanTest = recurse(); +})(); diff --git a/testing/web-platform/mozilla/tests/websockets/handlers/mozilla_push_dummy_wsh.py b/testing/web-platform/mozilla/tests/websockets/handlers/mozilla_push_dummy_wsh.py @@ -34,6 +34,7 @@ import ssl import threading from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse +from uuid import uuid4 from pywebsocket3 import msgutil @@ -73,6 +74,8 @@ class DummyEndpointHandler(BaseHTTPRequestHandler): "channelID": query["channelID"][0], "data": base64.urlsafe_b64encode(post_body).decode(), "headers": headers if len(post_body) > 0 else None, + # without a version string the push client thinks it's a duplicate + "version": str(uuid4()), } ), )