Bug 1673660 - C++ and JS API for Glean Custom Pings r=janerik,webidl,smaug
authorChris H-C <chutten@mozilla.com>
Mon, 14 Dec 2020 16:50:07 +0000
changeset 560667 f6c62f38a583ef1b7f33d7484afae6f6bf1eb031
parent 560666 551fd43769cad9f5242b8faaa31292a7b8526523
child 560668 a50052919e35a0d701120e122e40606153e48100
push id38032
push usercsabou@mozilla.com
push dateTue, 15 Dec 2020 09:29:54 +0000
treeherdermozilla-central@f805f27183c3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjanerik, webidl, smaug
bugs1673660
milestone85.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1673660 - C++ and JS API for Glean Custom Pings r=janerik,webidl,smaug Covers adding the new JS global `GleanPings` for JS, the new structs for C++ at mozilla::glean_pings, ping-id and string-table-index codegen, the usual boilerplate for JS and C++ stuff, and tests. Unresolved: * What happens if we call this on a non-parent process? (This isn't a supported mode of operation) Differential Revision: https://phabricator.services.mozilla.com/D98671
build/sparse-profiles/sphinx-docs
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowInner.h
dom/bindings/Bindings.conf
dom/chrome-webidl/GleanPings.webidl
dom/chrome-webidl/moz.build
dom/webidl/Window.webidl
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/xpcprivate.h
toolkit/components/glean/api/src/ffi/mod.rs
toolkit/components/glean/bindings/GleanPings.cpp
toolkit/components/glean/bindings/GleanPings.h
toolkit/components/glean/bindings/private/Ping.cpp
toolkit/components/glean/bindings/private/Ping.h
toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py
toolkit/components/glean/build_scripts/glean_parser_ext/js.py
toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2
toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2
toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2
toolkit/components/glean/build_scripts/glean_parser_ext/util.py
toolkit/components/glean/gtest/TestFog.cpp
toolkit/components/glean/metrics_index.py
toolkit/components/glean/moz.build
toolkit/components/glean/pytest/pings_test_output
toolkit/components/glean/pytest/pings_test_output_cpp
toolkit/components/glean/pytest/pings_test_output_js
toolkit/components/glean/pytest/test_glean_parser_cpp.py
toolkit/components/glean/pytest/test_glean_parser_js.py
toolkit/components/glean/test_pings.yaml
toolkit/components/glean/xpcom/nsIGleanMetrics.idl
toolkit/components/glean/xpcshell/test_Glean.js
toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
--- a/build/sparse-profiles/sphinx-docs
+++ b/build/sparse-profiles/sphinx-docs
@@ -31,8 +31,9 @@ path:config/milestone.txt
 
 # metrics.yaml and pings.yaml files (and their index) are needed to generate
 # Glean autodocs
 glob:**/metrics.yaml
 glob:**/pings.yaml
 path:toolkit/components/glean/metrics_index.py
 # TODO(bug 1672716): Make it easier to use other file names
 path:toolkit/components/glean/test_metrics.yaml
+path:toolkit/components/glean/test_pings.yaml
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -1308,16 +1308,17 @@ void nsGlobalWindowInner::FreeInnerObjec
   mSharedWorkers.Clear();
 
 #ifdef MOZ_WEBSPEECH
   mSpeechSynthesis = nullptr;
 #endif
 
 #ifdef MOZ_GLEAN
   mGlean = nullptr;
+  mGleanPings = nullptr;
 #endif
 
   mParentTarget = nullptr;
 
   if (mCleanMessageManager) {
     MOZ_ASSERT(mIsChrome, "only chrome should have msg manager cleaned");
     if (mChromeFields.mMessageManager) {
       mChromeFields.mMessageManager->Disconnect();
@@ -1402,16 +1403,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPerformance)
 
 #ifdef MOZ_WEBSPEECH
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSpeechSynthesis)
 #endif
 
 #ifdef MOZ_GLEAN
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlean)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGleanPings)
 #endif
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOuterWindow)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTopInnerWindow)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager)
 
@@ -1499,16 +1501,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance)
 
 #ifdef MOZ_WEBSPEECH
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSpeechSynthesis)
 #endif
 
 #ifdef MOZ_GLEAN
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlean)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mGleanPings)
 #endif
 
   if (tmp->mOuterWindow) {
     nsGlobalWindowOuter::Cast(tmp->mOuterWindow)->MaybeClearInnerWindow(tmp);
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mOuterWindow)
   }
 
   if (tmp->mListenerManager) {
@@ -2826,16 +2829,24 @@ bool nsGlobalWindowInner::HasActiveSpeec
 #ifdef MOZ_GLEAN
 mozilla::glean::Glean* nsGlobalWindowInner::Glean() {
   if (!mGlean) {
     mGlean = new mozilla::glean::Glean();
   }
 
   return mGlean;
 }
+
+mozilla::glean::GleanPings* nsGlobalWindowInner::GleanPings() {
+  if (!mGleanPings) {
+    mGleanPings = new mozilla::glean::GleanPings();
+  }
+
+  return mGleanPings;
+}
 #endif
 
 Nullable<WindowProxyHolder> nsGlobalWindowInner::GetParent(
     ErrorResult& aError) {
   FORWARD_TO_OUTER_OR_THROW(GetParentOuter, (), aError, nullptr);
 }
 
 /**
--- a/dom/base/nsGlobalWindowInner.h
+++ b/dom/base/nsGlobalWindowInner.h
@@ -47,16 +47,17 @@
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/TimeStamp.h"
 #include "nsWrapperCacheInlines.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/WindowProxyHolder.h"
 #ifdef MOZ_GLEAN
 #  include "mozilla/glean/bindings/Glean.h"
+#  include "mozilla/glean/bindings/GleanPings.h"
 #endif
 #include "Units.h"
 #include "nsComponentManagerUtils.h"
 #include "nsSize.h"
 #include "nsCheapSets.h"
 #include "mozilla/dom/ImageBitmapSource.h"
 #include "mozilla/UniquePtr.h"
 #include "nsRefreshObservers.h"
@@ -834,16 +835,17 @@ class nsGlobalWindowInner final : public
 #ifdef MOZ_WEBSPEECH
   mozilla::dom::SpeechSynthesis* GetSpeechSynthesis(
       mozilla::ErrorResult& aError);
   bool HasActiveSpeechSynthesis();
 #endif
 
 #ifdef MOZ_GLEAN
   mozilla::glean::Glean* Glean();
+  mozilla::glean::GleanPings* GleanPings();
 #endif
   already_AddRefed<nsICSSDeclaration> GetDefaultComputedStyle(
       mozilla::dom::Element& aElt, const nsAString& aPseudoElt,
       mozilla::ErrorResult& aError);
   void SizeToContent(mozilla::dom::CallerType aCallerType,
                      mozilla::ErrorResult& aError);
   mozilla::dom::Crypto* GetCrypto(mozilla::ErrorResult& aError);
   mozilla::dom::U2F* GetU2f(mozilla::ErrorResult& aError);
@@ -1440,16 +1442,17 @@ class nsGlobalWindowInner final : public
 #endif
 
 #ifdef MOZ_WEBSPEECH
   RefPtr<mozilla::dom::SpeechSynthesis> mSpeechSynthesis;
 #endif
 
 #ifdef MOZ_GLEAN
   RefPtr<mozilla::glean::Glean> mGlean;
+  RefPtr<mozilla::glean::GleanPings> mGleanPings;
 #endif
 
   // This is the CC generation the last time we called CanSkip.
   uint32_t mCanSkipCCGeneration;
 
   // The VR Displays for this window
   nsTArray<RefPtr<mozilla::dom::VRDisplay>> mVRDisplays;
 
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -1463,16 +1463,20 @@ DOMInterfaces = {
 'GleanImpl': {
     'nativeType': 'mozilla::glean::Glean',
     'headerFile': 'mozilla/glean/bindings/Glean.h',
 },
 'GleanCategory': {
     'nativeType': 'mozilla::glean::Category',
     'headerFile': 'mozilla/glean/bindings/Category.h',
 },
+'GleanPingsImpl': {
+    'nativeType': 'mozilla::glean::GleanPings',
+    'headerFile': 'mozilla/glean/bindings/GleanPings.h',
+},
 
 # WebRTC
 
 'WebrtcGlobalInformation': {
     'nativeType': 'mozilla::dom::WebrtcGlobalInformation',
     'headerFile': 'WebrtcGlobalInformation.h',
 },
 
@@ -1940,8 +1944,10 @@ addExternalIface('nsISHistory', nativeTy
 addExternalIface('ReferrerInfo', nativeType='nsIReferrerInfo')
 addExternalIface('nsIPermissionDelegateHandler',
                  nativeType='nsIPermissionDelegateHandler',
                  notflattened=True)
 addExternalIface('nsIOpenWindowInfo', nativeType='nsIOpenWindowInfo',
                  notflattened=True)
 addExternalIface('nsICookieJarSettings', nativeType='nsICookieJarSettings',
                  notflattened=True)
+addExternalIface('nsIGleanPing', headerFile='mozilla/glean/bindings/Ping.h',
+                 nativeType='nsIGleanPing', notflattened=True)
new file mode 100644
--- /dev/null
+++ b/dom/chrome-webidl/GleanPings.webidl
@@ -0,0 +1,15 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ */
+
+interface nsIGleanPing;
+
+[ChromeOnly, Exposed=Window]
+interface GleanPingsImpl {
+  /**
+   * Get a ping by name.
+   */
+  getter nsIGleanPing (DOMString identifier);
+};
--- a/dom/chrome-webidl/moz.build
+++ b/dom/chrome-webidl/moz.build
@@ -86,13 +86,14 @@ if CONFIG["MOZ_PLACES"]:
     WEBIDL_FILES += [
         "PlacesEvent.webidl",
         "PlacesObservers.webidl",
     ]
 
 if CONFIG["MOZ_GLEAN"]:
     WEBIDL_FILES += [
         "Glean.webidl",
+        "GleanPings.webidl",
     ]
 
 WEBIDL_FILES += [
     "PrioEncoder.webidl",
 ]
--- a/dom/webidl/Window.webidl
+++ b/dom/webidl/Window.webidl
@@ -702,16 +702,18 @@ partial interface Window {
   Promise<any> promiseDocumentFlushed(PromiseDocumentFlushedCallback callback);
 
   [ChromeOnly]
   readonly attribute boolean isChromeWindow;
 
 #ifdef MOZ_GLEAN
   [ChromeOnly]
   readonly attribute GleanImpl Glean;
+  [ChromeOnly]
+  readonly attribute GleanPingsImpl GleanPings;
 #endif
 };
 
 partial interface Window {
   [Pref="dom.vr.enabled"]
   attribute EventHandler onvrdisplayconnect;
   [Pref="dom.vr.enabled"]
   attribute EventHandler onvrdisplaydisconnect;
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -73,16 +73,17 @@
 #include "mozilla/dom/URLBinding.h"
 #include "mozilla/dom/URLSearchParamsBinding.h"
 #include "mozilla/dom/XMLHttpRequest.h"
 #include "mozilla/dom/XMLSerializerBinding.h"
 #include "mozilla/dom/FormDataBinding.h"
 #include "mozilla/dom/nsCSPContext.h"
 #ifdef MOZ_GLEAN
 #  include "mozilla/glean/bindings/Glean.h"
+#  include "mozilla/glean/bindings/GleanPings.h"
 #endif
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/DeferredFinalize.h"
 #include "mozilla/ExtensionPolicyService.h"
 #include "mozilla/NullPrincipal.h"
 #include "mozilla/ResultExtensions.h"
 #include "mozilla/StaticPrefs_extensions.h"
 
@@ -911,16 +912,18 @@ bool xpc::GlobalProperties::Parse(JSCont
       fetch = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "indexedDB")) {
       indexedDB = true;
     } else if (JS_LinearStringEqualsLiteral(nameStr, "isSecureContext")) {
       isSecureContext = true;
 #ifdef MOZ_GLEAN
     } else if (JS_LinearStringEqualsLiteral(nameStr, "Glean")) {
       glean = true;
+    } else if (JS_LinearStringEqualsLiteral(nameStr, "GleanPings")) {
+      gleanPings = true;
 #endif
 #ifdef MOZ_WEBRTC
     } else if (JS_LinearStringEqualsLiteral(nameStr, "rtcIdentityProvider")) {
       rtcIdentityProvider = true;
 #endif
     } else {
       RootedString nameStr(cx, nameValue.toString());
       JS::UniqueChars name = JS_EncodeStringToUTF8(cx, nameStr);
@@ -1079,16 +1082,19 @@ bool xpc::GlobalProperties::Define(JSCon
 
 bool xpc::GlobalProperties::DefineInXPCComponents(JSContext* cx,
                                                   JS::HandleObject obj) {
   if (indexedDB && !IndexedDatabaseManager::DefineIndexedDB(cx, obj))
     return false;
 
 #ifdef MOZ_GLEAN
   if (glean && !mozilla::glean::Glean::DefineGlean(cx, obj)) return false;
+  if (gleanPings && !mozilla::glean::GleanPings::DefineGleanPings(cx, obj)) {
+    return false;
+  }
 #endif
 
   return Define(cx, obj);
 }
 
 bool xpc::GlobalProperties::DefineInSandbox(JSContext* cx,
                                             JS::HandleObject obj) {
   MOZ_ASSERT(IsSandbox(obj));
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -2257,16 +2257,17 @@ struct GlobalProperties {
   bool caches : 1;
   bool crypto : 1;
   bool fetch : 1;
   bool indexedDB : 1;
   bool isSecureContext : 1;
   bool rtcIdentityProvider : 1;
 #ifdef MOZ_GLEAN
   bool glean : 1;
+  bool gleanPings : 1;
 #endif
 
  private:
   bool Define(JSContext* cx, JS::HandleObject obj);
 };
 
 // Infallible.
 already_AddRefed<nsIXPCComponents_utils_Sandbox> NewSandboxConstructor();
--- a/toolkit/components/glean/api/src/ffi/mod.rs
+++ b/toolkit/components/glean/api/src/ffi/mod.rs
@@ -1,14 +1,15 @@
 // 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 https://mozilla.org/MPL/2.0/.
 
 #![cfg(feature = "with_gecko")]
 
+use crate::pings;
 use thin_vec::ThinVec;
 use {nsstring::nsACString, uuid::Uuid};
 
 #[macro_use]
 mod macros;
 mod event;
 
 #[no_mangle]
@@ -178,8 +179,18 @@ pub extern "C" fn fog_memory_distributio
     }
 }
 
 #[no_mangle]
 pub extern "C" fn fog_memory_distribution_accumulate(id: u32, sample: u64) {
     let metric = metric_get!(MEMORY_DISTRIBUTION_MAP, id);
     metric.accumulate(sample);
 }
+
+#[no_mangle]
+pub extern "C" fn fog_submit_ping_by_id(id: u32, reason: &nsACString) {
+    let reason = if reason.is_empty() {
+        None
+    } else {
+        Some(reason.to_utf8())
+    };
+    pings::submit_ping_by_id(id, reason.as_deref());
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanPings.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/glean/bindings/GleanPings.h"
+
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMJSClass.h"
+#include "mozilla/dom/GleanPingsBinding.h"
+#include "mozilla/glean/bindings/GleanJSPingsLookup.h"
+#include "mozilla/glean/bindings/Ping.h"
+#include "MainThreadUtils.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GleanPings)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(GleanPings)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(GleanPings)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GleanPings)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* GleanPings::WrapObject(JSContext* aCx,
+                                 JS::Handle<JSObject*> aGivenProto) {
+  return dom::GleanPingsImpl_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// static
+bool GleanPings::DefineGleanPings(JSContext* aCx,
+                                  JS::Handle<JSObject*> aGlobal) {
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(JS::GetClass(aGlobal)->flags & JSCLASS_DOM_GLOBAL,
+             "Passed object is not a global object!");
+
+  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+  if (NS_WARN_IF(!global)) {
+    return false;
+  }
+
+  JS::Rooted<JS::Value> gleanPings(aCx);
+  js::AssertSameCompartment(aCx, aGlobal);
+
+  auto impl = MakeRefPtr<GleanPings>();
+  if (!dom::GetOrCreateDOMReflector(aCx, impl.get(), &gleanPings)) {
+    return false;
+  }
+
+  return JS_DefineProperty(aCx, aGlobal, "GleanPings", gleanPings,
+                           JSPROP_ENUMERATE);
+}
+
+already_AddRefed<GleanPing> GleanPings::NamedGetter(const nsAString& aName,
+                                                    bool& aFound) {
+  Maybe<uint32_t> pingId = PingByNameLookup(NS_ConvertUTF16toUTF8(aName));
+  if (pingId.isNothing()) {
+    aFound = false;
+    return nullptr;
+  }
+
+  aFound = true;
+  return MakeAndAddRef<GleanPing>(pingId.value());
+}
+
+bool GleanPings::NameIsEnumerable(const nsAString& aName) { return false; }
+
+void GleanPings::GetSupportedNames(nsTArray<nsString>& aNames) {
+  for (uint8_t idx : sPingByNameLookupEntries) {
+    const char* pingName = GetPingName(idx);
+    aNames.AppendElement()->AssignASCII(pingName);
+  }
+}
+
+}  // namespace mozilla::glean
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/GleanPings.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_glean_GleanPings_h
+#define mozilla_glean_GleanPings_h
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/glean/bindings/Ping.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::glean {
+
+class GleanPings final : public nsISupports, public nsWrapperCache {
+ public:
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(GleanPings)
+
+  JSObject* WrapObject(JSContext* aCx,
+                       JS::Handle<JSObject*> aGivenProto) override;
+  nsISupports* GetParentObject() { return nullptr; }
+
+  static bool DefineGleanPings(JSContext* aCx, JS::Handle<JSObject*> aGlobal);
+
+  already_AddRefed<GleanPing> NamedGetter(const nsAString& aName, bool& aFound);
+  bool NameIsEnumerable(const nsAString& aName);
+  void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ protected:
+  virtual ~GleanPings() = default;
+};
+
+}  // namespace mozilla::glean
+
+#endif /* mozilla_glean_GleanPings */
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Ping.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "mozilla/glean/bindings/Ping.h"
+
+#include "mozilla/Components.h"
+#include "nsIClassInfoImpl.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CLASSINFO(GleanPing, nullptr, 0, {0})
+NS_IMPL_ISUPPORTS_CI(GleanPing, nsIGleanPing)
+
+NS_IMETHODIMP
+GleanPing::Submit(const nsACString& aReason) {
+  mPing.Submit(aReason);
+  return NS_OK;
+}
+
+}  // namespace mozilla::glean
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Ping.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_glean_Ping_h
+#define mozilla_glean_Ping_h
+
+#include "mozilla/glean/fog_ffi_generated.h"
+#include "mozilla/Maybe.h"
+#include "nsIGleanMetrics.h"
+#include "nsString.h"
+
+namespace mozilla::glean {
+
+namespace impl {
+
+class Ping {
+ public:
+  constexpr explicit Ping(uint32_t aId) : mId(aId) {}
+
+  /**
+   * Collect and submit the ping for eventual upload.
+   *
+   * This will collect all stored data to be included in the ping.
+   * Data with lifetime `ping` will then be reset.
+   *
+   * If the ping is configured with `send_if_empty = false`
+   * and the ping currently contains no content,
+   * it will not be queued for upload.
+   * If the ping is configured with `send_if_empty = true`
+   * it will be queued for upload even if empty.
+   *
+   * Pings always contain the `ping_info` and `client_info` sections.
+   * See [ping
+   * sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections)
+   * for details.
+   *
+   * @param aReason - Optional. The reason the ping is being submitted.
+   *                  Must match one of the configured `reason_codes`.
+   */
+  void Submit(const nsACString& aReason) const {
+    fog_submit_ping_by_id(mId, &aReason);
+  }
+
+ private:
+  const uint32_t mId;
+};
+
+}  // namespace impl
+
+class GleanPing final : public nsIGleanPing {
+ public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIGLEANPING
+
+  explicit GleanPing(uint32_t aId) : mPing(aId) {}
+
+ private:
+  virtual ~GleanPing() = default;
+
+  const impl::Ping mPing;
+};
+
+}  // namespace mozilla::glean
+
+#endif /* mozilla_glean_Ping_h */
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/cpp.py
@@ -4,21 +4,50 @@
 # 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/.
 
 """
 Outputter to generate C++ code for metrics.
 """
 
 import jinja2
+import json
 
-from util import generate_metric_ids, is_implemented_metric_type
+from util import generate_metric_ids, generate_ping_ids, is_implemented_metric_type
 from glean_parser import util
 
 
+def cpp_datatypes_filter(value):
+    """
+    A Jinja2 filter that renders Rust literals.
+
+    Based on Python's JSONEncoder, but overrides:
+      - lists to array literals {}
+      - strings to "value"
+    """
+
+    class CppEncoder(json.JSONEncoder):
+        def iterencode(self, value):
+            if isinstance(value, list):
+                yield "{"
+                first = True
+                for subvalue in list(value):
+                    if not first:
+                        yield ", "
+                    yield from self.iterencode(subvalue)
+                    first = False
+                yield "}"
+            elif isinstance(value, str):
+                yield '"' + value + '"'
+            else:
+                yield from super().iterencode(value)
+
+    return "".join(CppEncoder().iterencode(value))
+
+
 def type_name(obj):
     """
     Returns the C++ type to use for a given metric object.
     """
 
     generate_enums = getattr(obj, "_generate_enums", [])  # Extra Keys? Reasons?
     if len(generate_enums):
         for name, suffix in generate_enums:
@@ -54,22 +83,30 @@ def output_cpp(objs, output_fd, options=
         env.filters["camelize"] = util.camelize
         env.filters["Camelize"] = util.Camelize
         for filter_name, filter_func in filters:
             env.filters[filter_name] = filter_func
         return env.get_template(template_name)
 
     util.get_jinja2_template = get_local_template
     get_metric_id = generate_metric_ids(objs)
+    get_ping_id = generate_ping_ids(objs)
 
-    template_filename = "cpp.jinja2"
+    if len(objs) == 1 and "pings" in objs:
+        template_filename = "cpp_pings.jinja2"
+    else:
+        template_filename = "cpp.jinja2"
+
     template = util.get_jinja2_template(
         template_filename,
         filters=(
+            ("cpp", cpp_datatypes_filter),
             ("snake_case", util.snake_case),
             ("type_name", type_name),
             ("metric_id", get_metric_id),
+            ("ping_id", get_ping_id),
             ("is_implemented_type", is_implemented_metric_type),
+            ("Camelize", util.Camelize),
         ),
     )
 
     output_fd.write(template.render(all_objs=objs))
     output_fd.write("\n")
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/js.py
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/js.py
@@ -12,17 +12,17 @@ string tables and mapping functions.
 The rest is handled by the WebIDL and XPIDL implementation
 that uses this code to look up metrics by name.
 """
 
 import jinja2
 from perfecthash import PerfectHash
 from string_table import StringTable
 
-from util import generate_metric_ids, is_implemented_metric_type
+from util import generate_metric_ids, generate_ping_ids, is_implemented_metric_type
 from glean_parser import util
 
 """
 We need to store several bits of information in the Perfect Hash Map Entry:
 
 1. An index into the string table to check for string equality with a search key
    The perfect hash function will give false-positive for non-existent keys,
    so we need to verify these ourselves.
@@ -40,16 +40,27 @@ ensures the generated C++ code follows.
 If we ever need more bits for a part (e.g. when we add the 33rd metric type),
 we figure out if either the string table indices or the range of possible IDs can be reduced
 and adjust the constants below.
 """
 ENTRY_WIDTH = 64
 INDEX_BITS = 32
 ID_BITS = 27
 
+PING_INDEX_BITS = 16
+
+
+def ping_entry(ping_id, ping_string_index):
+    """
+    The 2 pieces of information of a ping encoded into a single 32-bit integer.
+    """
+    assert ping_id < 2 ** (32 - PING_INDEX_BITS)
+    assert ping_string_index < 2 ** PING_INDEX_BITS
+    return ping_id << PING_INDEX_BITS | ping_string_index
+
 
 def create_entry(metric_id, type_id, idx):
     """
     The 3 pieces of information of a metric encoded into a single 64-bit integer.
     """
     return metric_id << INDEX_BITS | type_id << (INDEX_BITS + ID_BITS) | idx
 
 
@@ -89,17 +100,28 @@ def output_js(objs, output_fd, options={
             lstrip_blocks=True,
         )
         env.filters["Camelize"] = util.Camelize
         for filter_name, filter_func in filters:
             env.filters[filter_name] = filter_func
         return env.get_template(template_name)
 
     util.get_jinja2_template = get_local_template
-    template_filename = "js.jinja2"
+
+    if len(objs) == 1 and "pings" in objs:
+        write_pings(objs, output_fd, "js_pings.jinja2")
+    else:
+        write_metrics(objs, output_fd, "js.jinja2")
+
+
+def write_metrics(objs, output_fd, template_filename):
+    """
+    Given a tree of objects `objs`, output metrics-only code for the JS API to the
+    file-like object `output_fd` using template `template_filename`
+    """
 
     template = util.get_jinja2_template(
         template_filename,
         filters=(
             ("type_name", type_name),
             ("is_implemented_type", is_implemented_metric_type),
         ),
     )
@@ -179,8 +201,56 @@ def output_js(objs, output_fd, options={
             id_bits=ID_BITS,
             category_string_table=category_string_table,
             category_by_name_lookup=category_by_name_lookup,
             metric_string_table=metric_string_table,
             metric_by_name_lookup=metric_by_name_lookup,
         )
     )
     output_fd.write("\n")
+
+
+def write_pings(objs, output_fd, template_filename):
+    """
+    Given a tree of objects `objs`, output pings-only code for the JS API to the
+    file-like object `output_fd` using template `template_filename`
+    """
+
+    template = util.get_jinja2_template(
+        template_filename,
+        filters=(),
+    )
+
+    ping_string_table = StringTable()
+    get_ping_id = generate_ping_ids(objs)
+    # The map of a ping's name to its entry (a combination of a monotonic
+    # integer and its index in the string table)
+    pings = {}
+    for ping_name in objs["pings"].keys():
+        ping_id = get_ping_id(ping_name)
+        ping_name = util.camelize(ping_name)
+        pings[ping_name] = ping_entry(ping_id, ping_string_table.stringIndex(ping_name))
+
+    ping_map = [
+        (bytearray(ping_name, "ascii"), ping_entry)
+        for (ping_name, ping_entry) in pings.items()
+    ]
+    ping_string_table = ping_string_table.writeToString("gPingStringTable")
+    ping_phf = PerfectHash(ping_map, 64)
+    ping_by_name_lookup = ping_phf.cxx_codegen(
+        name="PingByNameLookup",
+        entry_type="ping_entry_t",
+        lower_entry=lambda x: str(x[1]),
+        key_type="const nsACString&",
+        key_bytes="aKey.BeginReading()",
+        key_length="aKey.Length()",
+        return_type="static Maybe<uint32_t>",
+        return_entry="return ping_result_check(aKey, entry);",
+    )
+
+    output_fd.write(
+        template.render(
+            ping_index_bits=PING_INDEX_BITS,
+            ping_by_name_lookup=ping_by_name_lookup,
+            ping_string_table=ping_string_table,
+        )
+    )
+    output_fd.write("\n")
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/rust.py
@@ -8,17 +8,17 @@
 Outputter to generate Rust code for metrics.
 """
 
 import enum
 import json
 
 import jinja2
 
-from util import generate_metric_ids
+from util import generate_metric_ids, generate_ping_ids
 from glean_parser import util
 
 
 def rust_datatypes_filter(value):
     """
     A Jinja2 filter that renders Rust literals.
 
     Based on Python's JSONEncoder, but overrides:
@@ -135,16 +135,17 @@ def output_rust(objs, output_fd, options
         env.filters["camelize"] = util.camelize
         env.filters["Camelize"] = util.Camelize
         for filter_name, filter_func in filters:
             env.filters[filter_name] = filter_func
         return env.get_template(template_name)
 
     util.get_jinja2_template = get_local_template
     get_metric_id = generate_metric_ids(objs)
+    get_ping_id = generate_ping_ids(objs)
 
     # Map from a tuple (const, typ) to an array of tuples (id, path)
     # where:
     #   const: The Rust constant name to be used for the lookup map
     #   typ:   The metric type to be stored in the lookup map
     #   id:    The numeric metric ID
     #   path:  The fully qualified path to the metric object in Rust
     #
@@ -193,16 +194,17 @@ def output_rust(objs, output_fd, options
         template_filename,
         filters=(
             ("rust", rust_datatypes_filter),
             ("snake_case", util.snake_case),
             ("type_name", type_name),
             ("ctor", ctor),
             ("extra_keys", extra_keys),
             ("metric_id", get_metric_id),
+            ("ping_id", get_ping_id),
         ),
     )
 
     # The list of all args to CommonMetricData (besides category and name).
     # No particular order is required, but I have these in common_metric_data.rs
     # order just to be organized.
     common_metric_data_args = [
         "name",
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/cpp_pings.jinja2
@@ -0,0 +1,30 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser.  DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+
+#ifndef mozilla_glean_Pings_h
+#define mozilla_glean_Pings_h
+
+#include "mozilla/glean/bindings/Ping.h"
+
+namespace mozilla::glean_pings {
+
+{% for obj in all_objs['pings'].values() %}
+/*
+ * Generated from {{ obj.name }}.
+ *
+ * {{ obj.description|wordwrap() | replace('\n', '\n * ') }}
+ */
+constexpr glean::impl::Ping {{ obj.name|Camelize }}({{ obj.name|ping_id }});
+
+{% endfor %}
+
+}  // namespace mozilla::glean_pings
+
+#endif  // mozilla_glean_Pings_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/js_pings.jinja2
@@ -0,0 +1,64 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser.  DO NOT EDIT.
+{# The rendered source is autogenerated, but this
+Jinja2 template is not. Please file bugs! #}
+
+/* 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/. */
+
+#ifndef mozilla_GleanJSPingsLookup_h
+#define mozilla_GleanJSPingsLookup_h
+
+#define GLEAN_PING_INDEX_BITS ({{ping_index_bits}})
+#define GLEAN_PING_ID(entry) ((entry) >> GLEAN_PING_INDEX_BITS)
+#define GLEAN_PING_INDEX(entry) ((entry) & ((1UL << GLEAN_PING_INDEX_BITS) - 1))
+
+namespace mozilla::glean {
+
+// Contains the ping id and the index into the ping string table.
+using ping_entry_t = uint32_t;
+
+static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry);
+
+{{ ping_string_table }}
+
+{{ ping_by_name_lookup }}
+
+/**
+ * Get a ping's name given its entry from the PHF.
+ */
+static const char* GetPingName(ping_entry_t aEntry) {
+  uint32_t idx = GLEAN_PING_INDEX(aEntry);
+  MOZ_ASSERT(idx < sizeof(gPingStringTable), "Ping index larger than string table");
+  return &gPingStringTable[idx];
+}
+
+/**
+ * Check if the found entry is pointing at the correct ping.
+ * PHF can false-positive a result when the key isn't present, so we check
+ * for a string match. If it fails, return Nothing(). If we found it,
+ * return the ping's id.
+ */
+static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry) {
+  uint32_t idx = GLEAN_PING_INDEX(aEntry);
+  uint32_t id = GLEAN_PING_ID(aEntry);
+
+  if (MOZ_UNLIKELY(idx > sizeof(gPingStringTable))) {
+    return Nothing();
+  }
+
+  if (aKey.EqualsASCII(&gPingStringTable[idx])) {
+    return Some(id);
+  }
+
+  return Nothing();
+}
+
+#undef GLEAN_PING_INDEX_BITS
+#undef GLEAN_PING_ID
+#undef GLEAN_PING_INDEX
+
+}  // namespace mozilla::glean
+#endif  // mozilla_GleanJSPingsLookup_h
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/templates/rust_pings.jinja2
@@ -19,8 +19,21 @@ pub static {{ obj.name|snake_case }}: La
         "{{ obj.name }}",
         {{ obj.include_client_id|rust }},
         {{ obj.send_if_empty|rust }},
         {{ obj.reason_codes|rust }},
     )
 });
 
 {% endfor %}
+
+#[cfg(feature = "with_gecko")]
+pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) {
+    match id {
+{% for obj in all_objs['pings'].values() %}
+        {{ obj.name|ping_id }} => {{ obj.name | snake_case }}.submit(reason),
+{% endfor %}
+        _ => {
+            // TODO: instrument this error.
+            log::error!("Cannot submit unknown ping {} by id.", id);
+        }
+    }
+}
--- a/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
+++ b/toolkit/components/glean/build_scripts/glean_parser_ext/util.py
@@ -4,27 +4,52 @@
 # 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/.
 
 """
 Utitlity functions for the glean_parser-based code generator
 """
 
 
+def generate_ping_ids(objs):
+    """
+    Return a lookup function for ping IDs per ping name.
+
+    :param objs: A tree of objects as returned from `parser.parse_objects`.
+    """
+
+    if "pings" not in objs:
+
+        def no_ping_ids_for_you():
+            assert False
+
+        return no_ping_ids_for_you
+
+    # Ping ID 0 is reserved (but unused) right now.
+    ping_id = 1
+
+    ping_id_mapping = {}
+    for ping_name in objs["pings"].keys():
+        ping_id_mapping[ping_name] = ping_id
+        ping_id += 1
+
+    return lambda ping_name: ping_id_mapping[ping_name]
+
+
 def generate_metric_ids(objs):
     """
     Return a lookup function for metric IDs per metric object.
 
     :param objs: A tree of metrics as returned from `parser.parse_objects`.
     """
 
     # Metric ID 0 is reserved (but unused) right now.
     metric_id = 1
 
-    # Mapping from a tuple of (categoru name, metric name) to the metric's numeric ID
+    # Mapping from a tuple of (category name, metric name) to the metric's numeric ID
     metric_id_mapping = {}
     for category_name, metrics in objs.items():
         for metric in metrics.values():
             metric_id_mapping[(category_name, metric.name)] = metric_id
             metric_id += 1
 
     return lambda metric: metric_id_mapping[(metric.category, metric.name)]
 
--- a/toolkit/components/glean/gtest/TestFog.cpp
+++ b/toolkit/components/glean/gtest/TestFog.cpp
@@ -1,20 +1,22 @@
 /* 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/. */
 
 #include "gtest/gtest.h"
 #include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/glean/GleanPings.h"
 #include "mozilla/glean/fog_ffi_generated.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Tuple.h"
 #include "nsTArray.h"
 
 #include "mozilla/Preferences.h"
+#include "mozilla/Unused.h"
 #include "nsString.h"
 #include "prtime.h"
 
 using mozilla::Preferences;
 using namespace mozilla::glean;
 using namespace mozilla::glean::impl;
 
 #define DATA_PREF "datareporting.healthreport.uploadEnabled"
@@ -151,8 +153,17 @@ TEST(FOG, TestCppMemoryDistWorks)
   for (auto iter = data.values.Iter(); !iter.Done(); iter.Next()) {
     const uint64_t bucket = iter.Key();
     const uint64_t count = iter.UserData();
     ASSERT_TRUE(count == 0 ||
                 (count == 1 && (bucket == 17520006 || bucket == 7053950)))
     << "Only two occupied buckets";
   }
 }
+
+TEST(FOG, TestCppPings)
+{
+  auto ping = mozilla::glean_pings::OnePingOnly;
+  mozilla::Unused << ping;
+  // That's it. That's the test. It will fail to compile if it's missing.
+  // For a test that actually submits the ping, we have integration tests.
+  // See also bug 1681742.
+}
--- a/toolkit/components/glean/metrics_index.py
+++ b/toolkit/components/glean/metrics_index.py
@@ -10,9 +10,10 @@ metrics_yamls = [
     "toolkit/components/glean/metrics.yaml",
     "toolkit/components/glean/test_metrics.yaml",
 ]
 
 # The list of all Glean pings.yaml files, relative to the top src dir.
 # New additions should be added to the bottom of the list.
 pings_yamls = [
     "toolkit/components/glean/pings.yaml",
+    "toolkit/components/glean/test_pings.yaml",
 ]
--- a/toolkit/components/glean/moz.build
+++ b/toolkit/components/glean/moz.build
@@ -19,49 +19,55 @@ if CONFIG["MOZ_GLEAN"]:
     FINAL_LIBRARY = "xul"
 
     EXPORTS.mozilla += [
         "ipc/FOGIPC.h",
     ]
 
     EXPORTS.mozilla.glean += [
         "!GleanMetrics.h",
+        "!GleanPings.h",
     ]
 
     EXPORTS.mozilla.glean.bindings += [
         "!GleanJSMetricsLookup.h",
+        "!GleanJSPingsLookup.h",
         "bindings/Category.h",
         "bindings/Glean.h",
+        "bindings/GleanPings.h",
         "bindings/MetricTypes.h",
         "bindings/private/Boolean.h",
         "bindings/private/Counter.h",
         "bindings/private/Datetime.h",
         "bindings/private/Event.h",
         "bindings/private/MemoryDistribution.h",
+        "bindings/private/Ping.h",
         "bindings/private/String.h",
         "bindings/private/Timespan.h",
         "bindings/private/Uuid.h",
     ]
 
     if CONFIG["COMPILE_ENVIRONMENT"]:
         EXPORTS.mozilla.glean += [
             "!fog_ffi_generated.h",
         ]
 
         CbindgenHeader("fog_ffi_generated.h", inputs=["/toolkit/components/glean"])
 
     UNIFIED_SOURCES += [
         "bindings/Category.cpp",
         "bindings/Glean.cpp",
+        "bindings/GleanPings.cpp",
         "bindings/private/Boolean.cpp",
         "bindings/private/Common.cpp",
         "bindings/private/Counter.cpp",
         "bindings/private/Datetime.cpp",
         "bindings/private/Event.cpp",
         "bindings/private/MemoryDistribution.cpp",
+        "bindings/private/Ping.cpp",
         "bindings/private/String.cpp",
         "bindings/private/Timespan.cpp",
         "bindings/private/Uuid.cpp",
         "ipc/FOGIPC.cpp",
     ]
 
     TEST_DIRS += [
         "gtest",
@@ -100,16 +106,32 @@ if CONFIG["MOZ_GLEAN"]:
 
     GeneratedFile(
         "api/src/pings.rs",
         script="build_scripts/glean_parser_ext/run_glean_parser.py",
         flags=[CONFIG["MOZ_APP_VERSION"]],
         inputs=["metrics_index.py"] + pings_yamls,
     )
 
+    GeneratedFile(
+        "GleanPings.h",
+        script="build_scripts/glean_parser_ext/run_glean_parser.py",
+        entry_point="cpp_metrics",
+        flags=[CONFIG["MOZ_APP_VERSION"]],
+        inputs=["metrics_index.py"] + pings_yamls,
+    )
+
+    GeneratedFile(
+        "GleanJSPingsLookup.h",
+        script="build_scripts/glean_parser_ext/run_glean_parser.py",
+        entry_point="js_metrics",
+        flags=[CONFIG["MOZ_APP_VERSION"]],
+        inputs=["metrics_index.py"] + pings_yamls,
+    )
+
     DIRS += [
         "xpcom",
     ]
 
 with Files("docs/**"):
     SCHEDULES.exclusive = ["docs"]
 
 with Files("**"):
--- a/toolkit/components/glean/pytest/pings_test_output
+++ b/toolkit/components/glean/pytest/pings_test_output
@@ -64,8 +64,21 @@ pub static not_deletion_request: Lazy<Pi
         "not-deletion-request",
         true,
         true,
         vec![],
     )
 });
 
 
+#[cfg(feature = "with_gecko")]
+pub(crate) fn submit_ping_by_id(id: u32, reason: Option<&str>) {
+    match id {
+        1 => not_baseline.submit(reason),
+        2 => not_metrics.submit(reason),
+        3 => not_events.submit(reason),
+        4 => not_deletion_request.submit(reason),
+        _ => {
+            // TODO: instrument this error.
+            log::error!("Cannot submit unknown ping {} by id.", id);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/pytest/pings_test_output_cpp
@@ -0,0 +1,62 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser.  DO NOT EDIT.
+
+/* 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/. */
+
+#ifndef mozilla_glean_Pings_h
+#define mozilla_glean_Pings_h
+
+#include "mozilla/glean/bindings/Ping.h"
+
+namespace mozilla::glean_pings {
+
+/*
+ * Generated from not-baseline.
+ *
+ * This ping is intended to provide metrics that are managed by the library
+ * itself, and not explicitly set by the application or included in the
+ * application's `metrics.yaml` file. The `baseline` ping is automatically sent
+ * when the application is moved to the background.
+ */
+constexpr glean::impl::Ping NotBaseline(1);
+
+/*
+ * Generated from not-metrics.
+ *
+ * The `metrics` ping is intended for all of the metrics that are explicitly set
+ * by the application or are included in the application's `metrics.yaml` file
+ * (except events). The reported data is tied to the ping's *measurement window*,
+ * which is the time between the collection of two `metrics` ping. Ideally, this
+ * window is expected to be about 24 hours, given that the collection is scheduled
+ * daily at 4AM. Data in the `ping_info` section of the ping can be used to infer
+ * the length of this window.
+ */
+constexpr glean::impl::Ping NotMetrics(2);
+
+/*
+ * Generated from not-events.
+ *
+ * The events ping's purpose is to transport all of the event metric information.
+ * The `events` ping is automatically sent when the application is moved to the
+ * background.
+ */
+constexpr glean::impl::Ping NotEvents(3);
+
+/*
+ * Generated from not-deletion-request.
+ *
+ * This ping is submitted when a user opts out of sending technical and
+ * interaction data to Mozilla. This ping is intended to communicate to the Data
+ * Pipeline that the user wishes to have their reported Telemetry data deleted. As
+ * such it attempts to send itself at the moment the user opts out of data
+ * collection.
+ */
+constexpr glean::impl::Ping NotDeletionRequest(4);
+
+
+}  // namespace mozilla::glean_pings
+
+#endif  // mozilla_glean_Pings_h
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/pytest/pings_test_output_js
@@ -0,0 +1,99 @@
+// -*- mode: C++ -*-
+
+// AUTOGENERATED BY glean_parser.  DO NOT EDIT.
+
+/* 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/. */
+
+#ifndef mozilla_GleanJSPingsLookup_h
+#define mozilla_GleanJSPingsLookup_h
+
+#define GLEAN_PING_INDEX_BITS (16)
+#define GLEAN_PING_ID(entry) ((entry) >> GLEAN_PING_INDEX_BITS)
+#define GLEAN_PING_INDEX(entry) ((entry) & ((1UL << GLEAN_PING_INDEX_BITS) - 1))
+
+namespace mozilla::glean {
+
+// Contains the ping id and the index into the ping string table.
+using ping_entry_t = uint32_t;
+
+static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry);
+
+#if defined(_MSC_VER) && !defined(__clang__)
+const char gPingStringTable[] = {
+#else
+constexpr char gPingStringTable[] = {
+#endif
+  /*     0 - "notBaseline" */ 'n', 'o', 't', 'B', 'a', 's', 'e', 'l', 'i', 'n', 'e', '\0',
+  /*    12 - "notMetrics" */ 'n', 'o', 't', 'M', 'e', 't', 'r', 'i', 'c', 's', '\0',
+  /*    23 - "notEvents" */ 'n', 'o', 't', 'E', 'v', 'e', 'n', 't', 's', '\0',
+  /*    33 - "notDeletionRequest" */ 'n', 'o', 't', 'D', 'e', 'l', 'e', 't', 'i', 'o', 'n', 'R', 'e', 'q', 'u', 'e', 's', 't', '\0',
+};
+
+
+
+const ping_entry_t sPingByNameLookupEntries[] = {
+  65536,
+  196631,
+  262177,
+  131084
+};
+
+
+
+static Maybe<uint32_t>
+PingByNameLookup(const nsACString& aKey)
+{
+  static const uint8_t BASES[] = {
+       0,   0,   0,   0,   0,   0,   0,   1,   0,   0,   0,   0,   0,   0,   0,   0,
+       0,   0,   0,   0,   0,   2,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   3,
+       0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
+  };
+  
+
+  const char* bytes = aKey.BeginReading();
+  size_t length = aKey.Length();
+  auto& entry = mozilla::perfecthash::Lookup(bytes, length, BASES,
+                                             sPingByNameLookupEntries);
+  return ping_result_check(aKey, entry);
+}
+
+
+/**
+ * Get a ping's name given its entry from the PHF.
+ */
+static const char* GetPingName(ping_entry_t aEntry) {
+  uint32_t idx = GLEAN_PING_INDEX(aEntry);
+  MOZ_ASSERT(idx < sizeof(gPingStringTable), "Ping index larger than string table");
+  return &gPingStringTable[idx];
+}
+
+/**
+ * Check if the found entry is pointing at the correct ping.
+ * PHF can false-positive a result when the key isn't present, so we check
+ * for a string match. If it fails, return Nothing(). If we found it,
+ * return the ping's id.
+ */
+static Maybe<uint32_t> ping_result_check(const nsACString& aKey, ping_entry_t aEntry) {
+  uint32_t idx = GLEAN_PING_INDEX(aEntry);
+  uint32_t id = GLEAN_PING_ID(aEntry);
+
+  if (MOZ_UNLIKELY(idx > sizeof(gPingStringTable))) {
+    return Nothing();
+  }
+
+  if (aKey.EqualsASCII(&gPingStringTable[idx])) {
+    return Some(id);
+  }
+
+  return Nothing();
+}
+
+#undef GLEAN_PING_INDEX_BITS
+#undef GLEAN_PING_ID
+#undef GLEAN_PING_INDEX
+
+}  // namespace mozilla::glean
+#endif  // mozilla_GleanJSPingsLookup_h
--- a/toolkit/components/glean/pytest/test_glean_parser_cpp.py
+++ b/toolkit/components/glean/pytest/test_glean_parser_cpp.py
@@ -41,10 +41,37 @@ def test_all_metric_types():
 
     with open(
         path.join(path.dirname(__file__), "metrics_test_output_cpp"), "r"
     ) as file:
         EXPECTED_CPP = file.read()
     assert output_fd.getvalue() == EXPECTED_CPP
 
 
+def test_fake_pings():
+    """Another similarly-fragile test.
+    It generates C++ for pings_test.yaml, comparing it byte-for-byte
+    with an expected output C++ file `pings_test_output_cpp`.
+    Expect it to be fragile.
+    To generate a new expected output file, edit t/c/g/metrics_index.py,
+    comment out all other ping yamls, and add one for
+    t/c/g/pytest/pings_test.yaml. Run `mach build` (it'll fail). Copy
+    objdir/t/c/g/GleanPings.h over pings_test_output_cpp.
+    (Don't forget to undo your edits to t/c/g/metrics_index.py)
+    """
+
+    options = {"allow_reserved": False}
+    input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))]
+
+    all_objs = parser.parse_objects(input_files, options)
+    assert not util.report_validation_errors(all_objs)
+    assert not lint.lint_metrics(all_objs.value, options)
+
+    output_fd = io.StringIO()
+    cpp.output_cpp(all_objs.value, output_fd, options)
+
+    with open(path.join(path.dirname(__file__), "pings_test_output_cpp"), "r") as file:
+        EXPECTED_CPP = file.read()
+    assert output_fd.getvalue() == EXPECTED_CPP
+
+
 if __name__ == "__main__":
     mozunit.main()
--- a/toolkit/components/glean/pytest/test_glean_parser_js.py
+++ b/toolkit/components/glean/pytest/test_glean_parser_js.py
@@ -39,10 +39,37 @@ def test_all_metric_types():
     output_fd = io.StringIO()
     js.output_js(all_objs.value, output_fd, options)
 
     with open(path.join(path.dirname(__file__), "metrics_test_output_js"), "r") as file:
         EXPECTED_JS = file.read()
     assert output_fd.getvalue() == EXPECTED_JS
 
 
+def test_fake_pings():
+    """Another similarly-fragile test.
+    It generates C++ for pings_test.yaml, comparing it byte-for-byte
+    with an expected output C++ file `pings_test_output_js`.
+    Expect it to be fragile.
+    To generate a new expected output file, edit t/c/g/metrics_index.py,
+    comment out all other ping yamls, and add one for
+    t/c/g/pytest/pings_test.yaml. Run `mach build` (it'll fail). Copy
+    objdir/t/c/g/GleanJSPingsLookup.h over pings_test_output_js.
+    (Don't forget to undo your edits to t/c/g/metrics_index.py)
+    """
+
+    options = {"allow_reserved": False}
+    input_files = [Path(path.join(path.dirname(__file__), "pings_test.yaml"))]
+
+    all_objs = parser.parse_objects(input_files, options)
+    assert not util.report_validation_errors(all_objs)
+    assert not lint.lint_metrics(all_objs.value, options)
+
+    output_fd = io.StringIO()
+    js.output_js(all_objs.value, output_fd, options)
+
+    with open(path.join(path.dirname(__file__), "pings_test_output_js"), "r") as file:
+        EXPECTED_JS = file.read()
+    assert output_fd.getvalue() == EXPECTED_JS
+
+
 if __name__ == "__main__":
     mozunit.main()
new file mode 100644
--- /dev/null
+++ b/toolkit/components/glean/test_pings.yaml
@@ -0,0 +1,25 @@
+# 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/.
+
+# This file defines the pings that are recorded by the Glean SDK. They are
+# automatically converted to platform-specific code at build time using the
+# `glean_parser` PyPI package.
+
+# This file is presently for Internal FOG Test Use Only.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/1-0-0
+
+one-ping-only:
+  description: |
+    This ping is for tests only.
+  include_client_id: false
+  send_if_empty: true
+  bugs:
+    - https://bugzilla.mozilla.org/1673660
+  data_reviews:
+    - https://bugzilla.mozilla.org/show_bug.cgi?id=1673660#c1
+  notification_emails:
+    - chutten@mozilla.com
+    - glean-team@mozilla.com
--- a/toolkit/components/glean/xpcom/nsIGleanMetrics.idl
+++ b/toolkit/components/glean/xpcom/nsIGleanMetrics.idl
@@ -115,16 +115,41 @@ interface nsIGleanMemoryDistribution : n
    * Parent process only. Panics in child processes.
    *
    * @return value of the stored metric, or Nothing() if there is no value.
    */
   [implicit_jscontext]
 	jsval testGetValue(in ACString aStorageName);
 };
 
+[scriptable, uuid(5223a48b-687d-47ff-a629-fd4a72d1ecfa)]
+interface nsIGleanPing : nsISupports
+{
+  /**
+   * Collect and submit the ping for eventual upload.
+   *
+   * This will collect all stored data to be included in the ping.
+   * Data with lifetime `ping` will then be reset.
+   *
+   * If the ping is configured with `send_if_empty = false`
+   * and the ping currently contains no content,
+   * it will not be queued for upload.
+   * If the ping is configured with `send_if_empty = true`
+   * it will be queued for upload even if empty.
+   *
+   * Pings always contain the `ping_info` and `client_info` sections.
+   * See [ping sections](https://mozilla.github.io/glean/book/user/pings/index.html#ping-sections)
+   * for details.
+   *
+   * @param aReason - Optional. The reason the ping is being submitted.
+   *                  Must match one of the configured `reason_codes`.
+   */
+	void submit([optional] in ACString aReason);
+};
+
 [scriptable, uuid(d84a3555-46f1-48c1-9122-e8e88b069d2b)]
 interface nsIGleanString : nsISupports
 {
   /*
   * Set to the specified value.
   *
   * @param value The string to set the metric to.
   */
--- a/toolkit/components/glean/xpcshell/test_Glean.js
+++ b/toolkit/components/glean/xpcshell/test_Glean.js
@@ -4,17 +4,17 @@
 /* FIXME: Remove these global markers.
  * FOG doesn't follow the stricter naming patterns as expected by tool configuration yet.
  * See https://searchfox.org/mozilla-central/source/.eslintrc.js#24
  * Reorganizing the directory structure will take this into account.
  */
 /* global add_task, Assert, do_get_profile */
 "use strict";
 
-Cu.importGlobalProperties(["Glean"]);
+Cu.importGlobalProperties(["Glean", "GleanPings"]);
 const { MockRegistrar } = ChromeUtils.import(
   "resource://testing-common/MockRegistrar.jsm"
 );
 const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
 
 /**
  * Mock the SysInfo object used to read System data in Gecko.
  */
@@ -157,8 +157,14 @@ add_task(async function test_fog_memory_
   Assert.equal(24 * 1024 * 1024, data.sum, "Sum's correct");
   for (let [bucket, count] of Object.entries(data.values)) {
     Assert.ok(
       count == 0 || (count == 1 && (bucket == 17520006 || bucket == 7053950)),
       "Only two buckets have a sample"
     );
   }
 });
+
+add_task(function test_fog_custom_pings() {
+  Assert.ok("onePingOnly" in GleanPings);
+  // Don't bother sending it, we'll test that in the integration suite.
+  // See also bug 1681742.
+});
--- a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
@@ -21,8 +21,9 @@ class FOGDocTypePingFilter(FOGPingFilter
         if not super(FOGDocTypePingFilter, self).__call__(ping):
             return False
 
         # Verify that the given ping was submitted to the URL for the doc_type
         return ping["request_url"]["doc_type"] == self.doc_type
 
 
 FOG_DELETION_REQUEST_PING = FOGDocTypePingFilter("deletion-request")
+FOG_ONE_PING_ONLY_PING = FOGDocTypePingFilter("one-ping-only")
--- a/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.ini
@@ -2,8 +2,9 @@
 tags = client
 
 [test_deletion_request_ping.py]
 [test_event_ping.py]
 [test_main_tab_scalars.py]
 [test_search_counts_across_sessions.py]
 [test_subsession_management.py]
 [test_fog_deletion_request_ping.py]
+[test_fog_custom_ping.py]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
@@ -0,0 +1,24 @@
+# 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/.
+
+from telemetry_harness.fog_ping_filters import FOG_ONE_PING_ONLY_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestDeletionRequestPing(FOGTestCase):
+    """Tests for the "one-ping-only" FOG custom ping."""
+
+    def test_one_ping_only_ping(self):
+        def send_opo_ping(marionette):
+            ping_sending_script = "GleanPings.onePingOnly.submit();"
+            with marionette.using_context(marionette.CONTEXT_CHROME):
+                marionette.execute_script(ping_sending_script)
+
+        ping1 = self.wait_for_ping(
+            lambda: send_opo_ping(self.marionette),
+            FOG_ONE_PING_ONLY_PING,
+            ping_server=self.fog_ping_server,
+        )
+
+        self.assertNotIn("client_id", ping1["payload"]["client_info"])