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:
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()),
}
),
)