Bug 1277806 - Implement keyed scalar measurements in Telemetry. r=gfritzsche,nfroyd
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Fri, 16 Sep 2016 03:43:00 +0200
changeset 314718 d99209b752fb
parent 314717 66a2fa62d760
child 314719 d0ad8d0da0ef
push id20583
push useralessio.placitelli@gmail.com
push dateWed, 21 Sep 2016 16:38:21 +0000
treeherderfx-team@73c0774056bb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgfritzsche, nfroyd
bugs1277806
milestone52.0a1
Bug 1277806 - Implement keyed scalar measurements in Telemetry. r=gfritzsche,nfroyd MozReview-Commit-ID: 9kkjpLAIQUX
toolkit/components/telemetry/Scalars.yaml
toolkit/components/telemetry/Telemetry.cpp
toolkit/components/telemetry/Telemetry.h
toolkit/components/telemetry/TelemetryScalar.cpp
toolkit/components/telemetry/TelemetryScalar.h
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/nsITelemetry.idl
toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -66,16 +66,68 @@ telemetry.test:
       - 1276190
     description: A testing scalar; not meant to be touched.
     expires: never
     kind: uint
     notification_emails:
       - telemetry-client-dev@mozilla.com
     release_channel_collection: opt-out
 
+  keyed_release_optin:
+    bug_numbers:
+      - 1277806
+    description: A testing scalar; not meant to be touched.
+    expires: never
+    kind: uint
+    keyed: true
+    notification_emails:
+      - telemetry-client-dev@mozilla.com
+    release_channel_collection: opt-in
+
+  keyed_release_optout:
+    bug_numbers:
+      - 1277806
+    description: A testing scalar; not meant to be touched.
+    expires: never
+    kind: uint
+    keyed: true
+    notification_emails:
+      - telemetry-client-dev@mozilla.com
+    release_channel_collection: opt-out
+
+  keyed_expired:
+    bug_numbers:
+      - 1277806
+    description: This is an expired testing scalar; not meant to be touched.
+    expires: 4.0a1
+    kind: uint
+    keyed: true
+    notification_emails:
+      - telemetry-client-dev@mozilla.com
+
+  keyed_unsigned_int:
+    bug_numbers:
+      - 1277806
+    description: A testing keyed uint scalar; not meant to be touched.
+    expires: never
+    kind: uint
+    keyed: true
+    notification_emails:
+      - telemetry-client-dev@mozilla.com
+
+  keyed_boolean_kind:
+    bug_numbers:
+      - 1277806
+    description: A testing keyed boolean scalar; not meant to be touched.
+    expires: never
+    kind: boolean
+    keyed: true
+    notification_emails:
+      - telemetry-client-dev@mozilla.com
+
 # The following section contains the browser engagement scalars.
 browser.engagement:
   max_concurrent_tab_count:
     bug_numbers:
       - 1271304
     description: >
       The count of maximum number of tabs open during a subsession,
       across all windows, including tabs in private windows and restored
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -2337,16 +2337,45 @@ TelemetryImpl::ScalarSetMaximum(const ns
 NS_IMETHODIMP
 TelemetryImpl::SnapshotScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
                                uint8_t optional_argc, JS::MutableHandleValue aResult)
 {
   return TelemetryScalar::CreateSnapshots(aDataset, aClearScalars, aCx, optional_argc, aResult);
 }
 
 NS_IMETHODIMP
+TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey,
+                              JS::HandleValue aVal, JSContext* aCx)
+{
+  return TelemetryScalar::Add(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey,
+                              JS::HandleValue aVal, JSContext* aCx)
+{
+  return TelemetryScalar::Set(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName, const nsAString& aKey,
+                              JS::HandleValue aVal, JSContext* aCx)
+{
+  return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotKeyedScalars(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+                                    uint8_t optional_argc, JS::MutableHandleValue aResult)
+{
+  return TelemetryScalar::CreateKeyedSnapshots(aDataset, aClearScalars, aCx, optional_argc,
+                                               aResult);
+}
+
+NS_IMETHODIMP
 TelemetryImpl::ClearScalars()
 {
   TelemetryScalar::ClearScalars();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 TelemetryImpl::FlushBatchedChildTelemetry()
@@ -2962,58 +2991,58 @@ void CreateStatisticsRecorder()
 
 void DestroyStatisticsRecorder()
 {
   TelemetryHistogram::DestroyStatisticsRecorder();
 }
 
 // Scalar API C++ Endpoints
 
-/**
- * Adds the value to the given scalar.
- *
- * @param aId The scalar enum id.
- * @param aValue The unsigned value to add to the scalar.
- */
 void
 ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
 {
   TelemetryScalar::Add(aId, aVal);
 }
 
-/**
- * Sets the scalar to the given value.
- *
- * @param aId The scalar enum id.
- * @param aValue The numeric, unsigned value to set the scalar to.
- */
 void
 ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
 {
   TelemetryScalar::Set(aId, aVal);
 }
 
-/**
- * Sets the scalar to the given value.
- *
- * @param aId The scalar enum id.
- * @param aValue The string value to set the scalar to.
- */
 void
 ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal)
 {
   TelemetryScalar::Set(aId, aVal);
 }
 
-/**
- * Sets the scalar to the maximum of the current and the passed value.
- *
- * @param aId The scalar enum id.
- * @param aValue The unsigned value to set the scalar to.
- */
 void
 ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal)
 {
   TelemetryScalar::SetMaximum(aId, aVal);
 }
 
+void
+ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+  TelemetryScalar::Add(aId, aKey, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+  TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void
+ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aVal)
+{
+  TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void
+ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aVal)
+{
+  TelemetryScalar::SetMaximum(aId, aKey, aVal);
+}
+
 } // namespace Telemetry
 } // namespace mozilla
--- a/toolkit/components/telemetry/Telemetry.h
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -358,42 +358,79 @@ void RecordThreadHangStats(ThreadHangSta
  * @param aProfileDir The profile directory whose lock attempt failed
  */
 void WriteFailedProfileLock(nsIFile* aProfileDir);
 
 /**
  * Adds the value to the given scalar.
  *
  * @param aId The scalar enum id.
- * @param aValue The unsigned value to add to the scalar.
+ * @param aValue The value to add to the scalar.
  */
 void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 
 /**
  * Sets the scalar to the given value.
  *
  * @param aId The scalar enum id.
- * @param aValue The numeric, unsigned value to set the scalar to.
+ * @param aValue The value to set the scalar to.
  */
 void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 
 /**
  * Sets the scalar to the given value.
  *
  * @param aId The scalar enum id.
- * @param aValue The string value to set the scalar to, truncated to
+ * @param aValue The value to set the scalar to, truncated to
  *        50 characters if exceeding that length.
  */
 void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
 
 /**
  * Sets the scalar to the maximum of the current and the passed value.
  *
  * @param aId The scalar enum id.
- * @param aValue The unsigned value the scalar is set to if its greater
+ * @param aValue The value the scalar is set to if its greater
  *        than the current value.
  */
 void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value the scalar is set to if its greater
+ *        than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
 } // namespace Telemetry
 } // namespace mozilla
 
 #endif // Telemetry_h__
--- a/toolkit/components/telemetry/TelemetryScalar.cpp
+++ b/toolkit/components/telemetry/TelemetryScalar.cpp
@@ -65,27 +65,32 @@ using mozilla::Telemetry::Common::IsInDa
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
 // PRIVATE TYPES
 
 namespace {
 
+const uint32_t kMaximumNumberOfKeys = 100;
+const uint32_t kMaximumKeyStringLength = 70;
 const uint32_t kMaximumStringValueLength = 50;
 const uint32_t kScalarCount =
   static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
 
 enum class ScalarResult : uint8_t {
   // Nothing went wrong.
   Ok,
   // General Scalar Errors
   OperationNotSupported,
   InvalidType,
   InvalidValue,
+  // Keyed Scalar Errors
+  KeyTooLong,
+  TooManyKeys,
   // String Scalar Errors
   StringTooLong,
   // Unsigned Scalar Errors
   UnsignedNegativeValue,
   UnsignedTruncatedValue
 };
 
 typedef nsBaseHashtableET<nsDepCharHashKey, mozilla::Telemetry::ScalarID>
@@ -106,17 +111,20 @@ MapToNsResult(ScalarResult aSr)
       return NS_OK;
     case ScalarResult::OperationNotSupported:
       return NS_ERROR_NOT_AVAILABLE;
     case ScalarResult::StringTooLong:
       // We don't want to throw if we're setting a string that is too long.
       return NS_OK;
     case ScalarResult::InvalidType:
     case ScalarResult::InvalidValue:
+    case ScalarResult::KeyTooLong:
       return NS_ERROR_ILLEGAL_VALUE;
+    case ScalarResult::TooManyKeys:
+      return NS_ERROR_FAILURE;
     case ScalarResult::UnsignedNegativeValue:
     case ScalarResult::UnsignedTruncatedValue:
       // We shouldn't throw if trying to set a negative number or are truncated,
       // only warn the user.
       return NS_OK;
   }
   return NS_ERROR_FAILURE;
 }
@@ -466,18 +474,246 @@ ScalarBoolean::GetValue(nsCOMPtr<nsIVari
 }
 
 size_t
 ScalarBoolean::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
 {
   return aMallocSizeOf(this);
 }
 
+/**
+ * Allocate a scalar class given the scalar info.
+ *
+ * @param aInfo The informations for the scalar coming from the definition file.
+ * @return nullptr if the scalar type is unknown, otherwise a valid pointer to the
+ *         scalar type.
+ */
+ScalarBase*
+internal_ScalarAllocate(uint32_t aScalarKind)
+{
+  ScalarBase* scalar = nullptr;
+  switch (aScalarKind) {
+  case nsITelemetry::SCALAR_COUNT:
+    scalar = new ScalarUnsigned();
+    break;
+  case nsITelemetry::SCALAR_STRING:
+    scalar = new ScalarString();
+    break;
+  case nsITelemetry::SCALAR_BOOLEAN:
+    scalar = new ScalarBoolean();
+    break;
+  default:
+    MOZ_ASSERT(false, "Invalid scalar type");
+  }
+  return scalar;
+}
+
+/**
+ * The implementation for the keyed scalar type.
+ */
+class KeyedScalar
+{
+public:
+  typedef mozilla::Pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
+
+  explicit KeyedScalar(uint32_t aScalarKind) : mScalarKind(aScalarKind) {};
+  ~KeyedScalar() {};
+
+  // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+  // These methods implicitly instantiate a Scalar[*] for each key.
+  ScalarResult SetValue(const nsAString& aKey, nsIVariant* aValue);
+  ScalarResult AddValue(const nsAString& aKey, nsIVariant* aValue);
+  ScalarResult SetMaximum(const nsAString& aKey, nsIVariant* aValue);
+
+  // Convenience methods used by the C++ API.
+  void SetValue(const nsAString& aKey, uint32_t aValue);
+  void SetValue(const nsAString& aKey, bool aValue);
+  void AddValue(const nsAString& aKey, uint32_t aValue);
+  void SetMaximum(const nsAString& aKey, uint32_t aValue);
+
+  // GetValue is used to get the key-value pairs stored in the keyed scalar
+  // when persisting it to JS.
+  nsresult GetValue(nsTArray<KeyValuePair>& aValues) const;
+
+  // To measure the memory stats.
+  size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+private:
+  typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
+
+  ScalarKeysMapType mScalarKeys;
+  const uint32_t mScalarKind;
+
+  ScalarResult GetScalarForKey(const nsAString& aKey, ScalarBase** aRet);
+};
+
+ScalarResult
+KeyedScalar::SetValue(const nsAString& aKey, nsIVariant* aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    return sr;
+  }
+
+  return scalar->SetValue(aValue);
+}
+
+ScalarResult
+KeyedScalar::AddValue(const nsAString& aKey, nsIVariant* aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    return sr;
+  }
+
+  return scalar->AddValue(aValue);
+}
+
+ScalarResult
+KeyedScalar::SetMaximum(const nsAString& aKey, nsIVariant* aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    return sr;
+  }
+
+  return scalar->SetMaximum(aValue);
+}
+
+void
+KeyedScalar::SetValue(const nsAString& aKey, uint32_t aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+    return;
+  }
+
+  return scalar->SetValue(aValue);
+}
+
+void
+KeyedScalar::SetValue(const nsAString& aKey, bool aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+    return;
+  }
+
+  return scalar->SetValue(aValue);
+}
+
+void
+KeyedScalar::AddValue(const nsAString& aKey, uint32_t aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+    return;
+  }
+
+  return scalar->AddValue(aValue);
+}
+
+void
+KeyedScalar::SetMaximum(const nsAString& aKey, uint32_t aValue)
+{
+  ScalarBase* scalar = nullptr;
+  ScalarResult sr = GetScalarForKey(aKey, &scalar);
+  if (sr != ScalarResult::Ok) {
+    MOZ_ASSERT(false, "Key too long or too many keys are recorded in the scalar.");
+    return;
+  }
+
+  return scalar->SetMaximum(aValue);
+}
+
+/**
+ * Get a key-value array with the values for the Keyed Scalar.
+ * @param aValue The array that will hold the key-value pairs.
+ * @return {nsresult} NS_OK or an error value as reported by the
+ *         the specific scalar objects implementations (e.g.
+ *         ScalarUnsigned).
+ */
+nsresult
+KeyedScalar::GetValue(nsTArray<KeyValuePair>& aValues) const
+{
+  for (auto iter = mScalarKeys.ConstIter(); !iter.Done(); iter.Next()) {
+    ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+
+    // Get the scalar value.
+    nsCOMPtr<nsIVariant> scalarValue;
+    nsresult rv = scalar->GetValue(scalarValue);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    // Append it to value list.
+    aValues.AppendElement(mozilla::MakePair(nsCString(iter.Key()), scalarValue));
+  }
+
+  return NS_OK;
+}
+
+/**
+ * Get the scalar for the referenced key.
+ * If there's no such key, instantiate a new Scalar object with the
+ * same type of the Keyed scalar and create the key.
+ */
+ScalarResult
+KeyedScalar::GetScalarForKey(const nsAString& aKey, ScalarBase** aRet)
+{
+  if (aKey.Length() >= kMaximumKeyStringLength) {
+    return ScalarResult::KeyTooLong;
+  }
+
+  if (mScalarKeys.Count() >= kMaximumNumberOfKeys) {
+    return ScalarResult::TooManyKeys;
+  }
+
+  NS_ConvertUTF16toUTF8 utf8Key(aKey);
+
+  ScalarBase* scalar = nullptr;
+  if (mScalarKeys.Get(utf8Key, &scalar)) {
+    *aRet = scalar;
+    return ScalarResult::Ok;
+  }
+
+  scalar = internal_ScalarAllocate(mScalarKind);
+  if (!scalar) {
+    return ScalarResult::InvalidType;
+  }
+
+  mScalarKeys.Put(utf8Key, scalar);
+
+  *aRet = scalar;
+  return ScalarResult::Ok;
+}
+
+size_t
+KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+{
+  size_t n = aMallocSizeOf(this);
+  for (auto iter = mScalarKeys.Iter(); !iter.Done(); iter.Next()) {
+    ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
+    n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+  }
+  return n;
+}
+
 typedef nsUint32HashKey ScalarIDHashKey;
 typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
+typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar> KeyedScalarStorageMapType;
 
 } // namespace
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
 // PRIVATE STATE, SHARED BY ALL THREADS
 
@@ -490,16 +726,19 @@ bool gCanRecordBase;
 bool gCanRecordExtended;
 
 // The Name -> ID cache map.
 ScalarMapType gScalarNameIDMap(kScalarCount);
 // The ID -> Scalar Object map. This is a nsClassHashtable, it owns
 // the scalar instance and takes care of deallocating them when they
 // get removed from the map.
 ScalarStorageMapType gScalarStorageMap;
+// The ID -> Keyed Scalar Object map. As for plain scalars, this is
+// nsClassHashtable. See above.
+KeyedScalarStorageMapType gKeyedScalarStorageMap;
 
 } // namespace
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
 // PRIVATE: Function that may call JS code.
 
@@ -546,22 +785,30 @@ internal_LogToBrowserConsole(uint32_t aL
  * Checks if the error should be logged.
  *
  * @param aSr The error code.
  * @return true if the error should be logged, false otherwise.
  */
 bool
 internal_ShouldLogError(ScalarResult aSr)
 {
-  if (aSr == ScalarResult::StringTooLong ||
-      aSr == ScalarResult::UnsignedNegativeValue ||
-      aSr == ScalarResult::UnsignedTruncatedValue) {
+  switch (aSr) {
+    case ScalarResult::StringTooLong: MOZ_FALLTHROUGH;
+    case ScalarResult::KeyTooLong: MOZ_FALLTHROUGH;
+    case ScalarResult::TooManyKeys: MOZ_FALLTHROUGH;
+    case ScalarResult::UnsignedNegativeValue: MOZ_FALLTHROUGH;
+    case ScalarResult::UnsignedTruncatedValue:
+      // Intentional fall-through.
       return true;
+
+    default:
+      return false;
   }
 
+  // It should never reach this point.
   return false;
 }
 
 /**
  * Converts the error code to a human readable error message and prints it to the
  * browser console.
  *
  * @param aScalarName The name of the scalar that raised the error.
@@ -572,16 +819,22 @@ internal_LogScalarError(const nsACString
 {
   nsAutoString errorMessage;
   AppendUTF8toUTF16(aScalarName, errorMessage);
 
   switch (aSr) {
     case ScalarResult::StringTooLong:
       errorMessage.Append(NS_LITERAL_STRING(" - Truncating scalar value to 50 characters."));
       break;
+    case ScalarResult::KeyTooLong:
+      errorMessage.Append(NS_LITERAL_STRING(" - The key length must be limited to 70 characters."));
+      break;
+    case ScalarResult::TooManyKeys:
+      errorMessage.Append(NS_LITERAL_STRING(" - Keyed scalars cannot have more than 100 keys."));
+      break;
     case ScalarResult::UnsignedNegativeValue:
       errorMessage.Append(NS_LITERAL_STRING(" - Trying to set an unsigned scalar to a negative number."));
       break;
     case ScalarResult::UnsignedTruncatedValue:
       errorMessage.Append(NS_LITERAL_STRING(" - Truncating float/double number."));
       break;
     default:
       // Nothing.
@@ -613,16 +866,28 @@ internal_CanRecordExtended()
 }
 
 const ScalarInfo&
 internal_InfoForScalarID(mozilla::Telemetry::ScalarID aId)
 {
   return gScalars[static_cast<uint32_t>(aId)];
 }
 
+/**
+ * Check if the given scalar is a keyed scalar.
+ *
+ * @param aId The scalar enum.
+ * @return true if aId refers to a keyed scalar, false otherwise.
+ */
+bool
+internal_IsKeyedScalar(mozilla::Telemetry::ScalarID aId)
+{
+  return internal_InfoForScalarID(aId).keyed;
+}
+
 bool
 internal_CanRecordForScalarID(mozilla::Telemetry::ScalarID aId)
 {
   // Get the scalar info from the id.
   const ScalarInfo &info = internal_InfoForScalarID(aId);
 
   // Can we record at all?
   bool canRecordBase = internal_CanRecordBase();
@@ -636,43 +901,16 @@ internal_CanRecordForScalarID(mozilla::T
   if (!canRecordDataset) {
     return false;
   }
 
   return true;
 }
 
 /**
- * Allocate a scalar class given the scalar info.
- *
- * @param aInfo The informations for the scalar coming from the definition file.
- * @return nullptr if the scalar type is unknown, otherwise a valid pointer to the
- *         scalar type.
- */
-ScalarBase*
-internal_ScalarAllocate(const ScalarInfo& aInfo)
-{
-  ScalarBase* scalar = nullptr;
-  switch (aInfo.kind) {
-  case nsITelemetry::SCALAR_COUNT:
-    scalar = new ScalarUnsigned();
-    break;
-  case nsITelemetry::SCALAR_STRING:
-    scalar = new ScalarString();
-    break;
-  case nsITelemetry::SCALAR_BOOLEAN:
-    scalar = new ScalarBoolean();
-    break;
-  default:
-    MOZ_ASSERT(false, "Invalid scalar type");
-  }
-  return scalar;
-}
-
-/**
  * Get the scalar enum id from the scalar name.
  *
  * @param aName The scalar name.
  * @param aId The output variable to contain the enum.
  * @return
  *   NS_ERROR_FAILURE if this was called before init is completed.
  *   NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions.
  *   NS_OK if the scalar was found and aId contains a valid enum id.
@@ -703,16 +941,17 @@ internal_GetEnumByScalarName(const nsACS
  *   NS_ERROR_NOT_AVAILABLE if the scalar is expired.
  *   NS_OK if the scalar was found. If that's the case, aResult contains a
  *   valid pointer to a scalar type.
  */
 nsresult
 internal_GetScalarByEnum(mozilla::Telemetry::ScalarID aId, ScalarBase** aRet)
 {
   if (!IsValidEnumId(aId)) {
+    MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
     return NS_ERROR_INVALID_ARG;
   }
 
   const uint32_t id = static_cast<uint32_t>(aId);
 
   ScalarBase* scalar = nullptr;
   if (gScalarStorageMap.Get(id, &scalar)) {
     *aRet = scalar;
@@ -720,17 +959,17 @@ internal_GetScalarByEnum(mozilla::Teleme
   }
 
   const ScalarInfo &info = gScalars[id];
 
   if (IsExpiredVersion(info.expiration())) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  scalar = internal_ScalarAllocate(info);
+  scalar = internal_ScalarAllocate(info.kind);
   if (!scalar) {
     return NS_ERROR_INVALID_ARG;
   }
 
   gScalarStorageMap.Put(id, scalar);
 
   *aRet = scalar;
   return NS_OK;
@@ -748,16 +987,112 @@ internal_GetRecordableScalar(mozilla::Te
 {
   // Get the scalar by the enum (it also internally checks for aId validity).
   ScalarBase* scalar = nullptr;
   nsresult rv = internal_GetScalarByEnum(aId, &scalar);
   if (NS_FAILED(rv)) {
     return nullptr;
   }
 
+  if (internal_IsKeyedScalar(aId)) {
+    return nullptr;
+  }
+
+  // Are we allowed to record this scalar?
+  if (!internal_CanRecordForScalarID(aId)) {
+    return nullptr;
+  }
+
+  return scalar;
+}
+
+} // namespace
+
+
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the keyed scalars
+
+namespace {
+
+/**
+ * Get a keyed scalar object by its enum id. This implicitly allocates the keyed
+ * scalar object in the storage if it wasn't previously allocated.
+ *
+ * @param aId The scalar id.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ *   NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed string
+ *                        scalar.
+ *   NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ *   NS_OK if the scalar was found. If that's the case, aResult contains a
+ *   valid pointer to a scalar type.
+ */
+nsresult
+internal_GetKeyedScalarByEnum(mozilla::Telemetry::ScalarID aId, KeyedScalar** aRet)
+{
+  if (!IsValidEnumId(aId)) {
+    MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  const uint32_t id = static_cast<uint32_t>(aId);
+
+  KeyedScalar* scalar = nullptr;
+  if (gKeyedScalarStorageMap.Get(id, &scalar)) {
+    *aRet = scalar;
+    return NS_OK;
+  }
+
+  const ScalarInfo &info = gScalars[id];
+
+  if (IsExpiredVersion(info.expiration())) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // We don't currently support keyed string scalars. Disable them.
+  if (info.kind == nsITelemetry::SCALAR_STRING) {
+    MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  scalar = new KeyedScalar(info.kind);
+  if (!scalar) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  gKeyedScalarStorageMap.Put(id, scalar);
+
+  *aRet = scalar;
+  return NS_OK;
+}
+
+/**
+ * Get a keyed scalar object by its enum id, if we're allowed to record it.
+ *
+ * @param aId The scalar id.
+ * @return The KeyedScalar instance or nullptr if we're not allowed to record
+ *         the scalar.
+ */
+KeyedScalar*
+internal_GetRecordableKeyedScalar(mozilla::Telemetry::ScalarID aId)
+{
+  // Get the scalar by the enum (it also internally checks for aId validity).
+  KeyedScalar* scalar = nullptr;
+  nsresult rv = internal_GetKeyedScalarByEnum(aId, &scalar);
+  if (NS_FAILED(rv)) {
+    return nullptr;
+  }
+
+  if (!internal_IsKeyedScalar(aId)) {
+    return nullptr;
+  }
+
   // Are we allowed to record this scalar?
   if (!internal_CanRecordForScalarID(aId)) {
     return nullptr;
   }
 
   return scalar;
 }
 
@@ -807,16 +1142,17 @@ TelemetryScalar::InitializeGlobalState(b
 void
 TelemetryScalar::DeInitializeGlobalState()
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
   gCanRecordBase = false;
   gCanRecordExtended = false;
   gScalarNameIDMap.Clear();
   gScalarStorageMap.Clear();
+  gKeyedScalarStorageMap.Clear();
   gInitDone = false;
 }
 
 void
 TelemetryScalar::SetCanRecordBase(bool b)
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
   gCanRecordBase = b;
@@ -829,17 +1165,17 @@ TelemetryScalar::SetCanRecordExtended(bo
 }
 
 /**
  * Adds the value to the given scalar.
  *
  * @param aName The scalar name.
  * @param aVal The numeric value to add to the scalar.
  * @param aCx The JS context.
- * @return NS_OK if the value was added or if we're not allow to record to this
+ * @return NS_OK if the value was added or if we're not allowed to record to this
  *  dataset. Otherwise, return an error.
  */
 nsresult
 TelemetryScalar::Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx)
 {
   // Unpack the aVal to nsIVariant. This uses the JS context.
   nsCOMPtr<nsIVariant> unpackedVal;
   nsresult rv =
@@ -853,16 +1189,21 @@ TelemetryScalar::Add(const nsACString& a
     StaticMutexAutoLock locker(gTelemetryScalarsMutex);
 
     mozilla::Telemetry::ScalarID id;
     rv = internal_GetEnumByScalarName(aName, &id);
     if (NS_FAILED(rv)) {
       return rv;
     }
 
+    // We're trying to set a plain scalar, so make sure this is one.
+    if (internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
     // Are we allowed to record this scalar?
     if (!internal_CanRecordForScalarID(id)) {
       return NS_OK;
     }
 
     // Finally get the scalar.
     ScalarBase* scalar = nullptr;
     rv = internal_GetScalarByEnum(id, &scalar);
@@ -883,16 +1224,80 @@ TelemetryScalar::Add(const nsACString& a
   }
 
   return MapToNsResult(sr);
 }
 
 /**
  * Adds the value to the given scalar.
  *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ *  dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+                     JSContext* aCx)
+{
+  // Unpack the aVal to nsIVariant. This uses the JS context.
+  nsCOMPtr<nsIVariant> unpackedVal;
+  nsresult rv =
+    nsContentUtils::XPConnect()->JSToVariant(aCx, aVal,  getter_AddRefs(unpackedVal));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  ScalarResult sr;
+  {
+    StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+    mozilla::Telemetry::ScalarID id;
+    rv = internal_GetEnumByScalarName(aName, &id);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    // Make sure this is a keyed scalar.
+    if (!internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
+    // Are we allowed to record this scalar?
+    if (!internal_CanRecordForScalarID(id)) {
+      return NS_OK;
+    }
+
+    // Finally get the scalar.
+    KeyedScalar* scalar = nullptr;
+    rv = internal_GetKeyedScalarByEnum(id, &scalar);
+    if (NS_FAILED(rv)) {
+      // Don't throw on expired scalars.
+      if (rv == NS_ERROR_NOT_AVAILABLE) {
+        return NS_OK;
+      }
+      return rv;
+    }
+
+    sr = scalar->AddValue(aKey, unpackedVal);
+  }
+
+  // Warn the user about the error if we need to.
+  if (internal_ShouldLogError(sr)) {
+    internal_LogScalarError(aName, sr);
+  }
+
+  return MapToNsResult(sr);
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
  * @param aId The scalar enum id.
  * @param aVal The numeric value to add to the scalar.
  */
 void
 TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
 
@@ -900,16 +1305,37 @@ TelemetryScalar::Add(mozilla::Telemetry:
   if (!scalar) {
     return;
   }
 
   scalar->AddValue(aValue);
 }
 
 /**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void
+TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+                     uint32_t aValue)
+{
+  StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+  KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+  if (!scalar) {
+    return;
+  }
+
+  scalar->AddValue(aKey, aValue);
+}
+
+/**
  * Sets the scalar to the given value.
  *
  * @param aName The scalar name.
  * @param aVal The value to set the scalar to.
  * @param aCx The JS context.
  * @return NS_OK if the value was added or if we're not allow to record to this
  *  dataset. Otherwise, return an error.
  */
@@ -929,16 +1355,21 @@ TelemetryScalar::Set(const nsACString& a
     StaticMutexAutoLock locker(gTelemetryScalarsMutex);
 
     mozilla::Telemetry::ScalarID id;
     rv = internal_GetEnumByScalarName(aName, &id);
     if (NS_FAILED(rv)) {
       return rv;
     }
 
+    // We're trying to set a plain scalar, so make sure this is one.
+    if (internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
     // Are we allowed to record this scalar?
     if (!internal_CanRecordForScalarID(id)) {
       return NS_OK;
     }
 
     // Finally get the scalar.
     ScalarBase* scalar = nullptr;
     rv = internal_GetScalarByEnum(id, &scalar);
@@ -957,16 +1388,80 @@ TelemetryScalar::Set(const nsACString& a
   if (internal_ShouldLogError(sr)) {
     internal_LogScalarError(aName, sr);
   }
 
   return MapToNsResult(sr);
 }
 
 /**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ *  dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+                     JSContext* aCx)
+{
+  // Unpack the aVal to nsIVariant. This uses the JS context.
+  nsCOMPtr<nsIVariant> unpackedVal;
+  nsresult rv =
+    nsContentUtils::XPConnect()->JSToVariant(aCx, aVal,  getter_AddRefs(unpackedVal));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  ScalarResult sr;
+  {
+    StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+    mozilla::Telemetry::ScalarID id;
+    rv = internal_GetEnumByScalarName(aName, &id);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    // We're trying to set a keyed scalar. Report an error if this isn't one.
+    if (!internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
+    // Are we allowed to record this scalar?
+    if (!internal_CanRecordForScalarID(id)) {
+      return NS_OK;
+    }
+
+    // Finally get the scalar.
+    KeyedScalar* scalar = nullptr;
+    rv = internal_GetKeyedScalarByEnum(id, &scalar);
+    if (NS_FAILED(rv)) {
+      // Don't throw on expired scalars.
+      if (rv == NS_ERROR_NOT_AVAILABLE) {
+        return NS_OK;
+      }
+      return rv;
+    }
+
+    sr = scalar->SetValue(aKey, unpackedVal);
+  }
+
+  // Warn the user about the error if we need to.
+  if (internal_ShouldLogError(sr)) {
+    internal_LogScalarError(aName, sr);
+  }
+
+  return MapToNsResult(sr);
+}
+
+/**
  * Sets the scalar to the given numeric value.
  *
  * @param aId The scalar enum id.
  * @param aValue The numeric, unsigned value to set the scalar to.
  */
 void
 TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
 {
@@ -1014,16 +1509,58 @@ TelemetryScalar::Set(mozilla::Telemetry:
   if (!scalar) {
     return;
   }
 
   scalar->SetValue(aValue);
 }
 
 /**
+ * Sets the keyed scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+                     uint32_t aValue)
+{
+  StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+  KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+  if (!scalar) {
+    return;
+  }
+
+  scalar->SetValue(aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void
+TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+                     bool aValue)
+{
+  StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+  KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+  if (!scalar) {
+    return;
+  }
+
+  scalar->SetValue(aKey, aValue);
+}
+
+/**
  * Sets the scalar to the maximum of the current and the passed value.
  *
  * @param aName The scalar name.
  * @param aVal The numeric value to set the scalar to.
  * @param aCx The JS context.
  * @return NS_OK if the value was added or if we're not allow to record to this
  *  dataset. Otherwise, return an error.
  */
@@ -1043,16 +1580,21 @@ TelemetryScalar::SetMaximum(const nsACSt
     StaticMutexAutoLock locker(gTelemetryScalarsMutex);
 
     mozilla::Telemetry::ScalarID id;
     rv = internal_GetEnumByScalarName(aName, &id);
     if (NS_FAILED(rv)) {
       return rv;
     }
 
+    // Make sure this is not a keyed scalar.
+    if (internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
     // Are we allowed to record this scalar?
     if (!internal_CanRecordForScalarID(id)) {
       return NS_OK;
     }
 
     // Finally get the scalar.
     ScalarBase* scalar = nullptr;
     rv = internal_GetScalarByEnum(id, &scalar);
@@ -1073,16 +1615,80 @@ TelemetryScalar::SetMaximum(const nsACSt
   }
 
   return MapToNsResult(sr);
 }
 
 /**
  * Sets the scalar to the maximum of the current and the passed value.
  *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK if the value was added or if we're not allow to record to this
+ *  dataset. Otherwise, return an error.
+ */
+nsresult
+TelemetryScalar::SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+                            JSContext* aCx)
+{
+  // Unpack the aVal to nsIVariant. This uses the JS context.
+  nsCOMPtr<nsIVariant> unpackedVal;
+  nsresult rv =
+    nsContentUtils::XPConnect()->JSToVariant(aCx, aVal,  getter_AddRefs(unpackedVal));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  ScalarResult sr;
+  {
+    StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+    mozilla::Telemetry::ScalarID id;
+    rv = internal_GetEnumByScalarName(aName, &id);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    // Make sure this is a keyed scalar.
+    if (!internal_IsKeyedScalar(id)) {
+      return NS_ERROR_ILLEGAL_VALUE;
+    }
+
+    // Are we allowed to record this scalar?
+    if (!internal_CanRecordForScalarID(id)) {
+      return NS_OK;
+    }
+
+    // Finally get the scalar.
+    KeyedScalar* scalar = nullptr;
+    rv = internal_GetKeyedScalarByEnum(id, &scalar);
+    if (NS_FAILED(rv)) {
+      // Don't throw on expired scalars.
+      if (rv == NS_ERROR_NOT_AVAILABLE) {
+        return NS_OK;
+      }
+      return rv;
+    }
+
+    sr = scalar->SetMaximum(aKey, unpackedVal);
+  }
+
+  // Warn the user about the error if we need to.
+  if (internal_ShouldLogError(sr)) {
+    internal_LogScalarError(aName, sr);
+  }
+
+  return MapToNsResult(sr);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
  * @param aId The scalar enum id.
  * @param aValue The numeric value to set the scalar to.
  */
 void
 TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue)
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
 
@@ -1090,16 +1696,37 @@ TelemetryScalar::SetMaximum(mozilla::Tel
   if (!scalar) {
     return;
   }
 
   scalar->SetValue(aValue);
 }
 
 /**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void
+TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+                            uint32_t aValue)
+{
+  StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+  KeyedScalar* scalar = internal_GetRecordableKeyedScalar(aId);
+  if (!scalar) {
+    return;
+  }
+
+  scalar->SetValue(aKey, aValue);
+}
+
+/**
  * Serializes the scalars from the given dataset to a json-style object and resets them.
  * The returned structure looks like {"group1.probe":1,"group1.other_probe":false,...}.
  *
  * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
  * @param aClear Whether to clear out the scalars after snapshotting.
  */
 nsresult
 TelemetryScalar::CreateSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
@@ -1165,35 +1792,135 @@ TelemetryScalar::CreateSnapshots(unsigne
       return NS_ERROR_FAILURE;
     }
   }
 
   return NS_OK;
 }
 
 /**
+ * Serializes the scalars from the given dataset to a json-style object and resets them.
+ * The returned structure looks like:
+ *   { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... }
+ *
+ * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+ * @param aClear Whether to clear out the keyed scalars after snapshotting.
+ */
+nsresult
+TelemetryScalar::CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+                                      uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult)
+{
+  // If no arguments were passed in, apply the default value.
+  if (!optional_argc) {
+    aClearScalars = false;
+  }
+
+  JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+  if (!root_obj) {
+    return NS_ERROR_FAILURE;
+  }
+  aResult.setObject(*root_obj);
+
+  // Only lock the mutex while accessing our data, without locking any JS related code.
+  typedef mozilla::Pair<const char*, nsTArray<KeyedScalar::KeyValuePair>> DataPair;
+  nsTArray<DataPair> scalarsToReflect;
+  {
+    StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+    // Iterate the scalars in gKeyedScalarStorageMap. The storage may contain empty or yet
+    // to be initialized scalars.
+    for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+      KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
+
+      // Get the informations for this scalar.
+      const ScalarInfo& info = gScalars[iter.Key()];
+
+      // Serialize the scalar if it's in the desired dataset.
+      if (IsInDataset(info.dataset, aDataset)) {
+        // Get the keys for this scalar.
+        nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
+        nsresult rv = scalar->GetValue(scalarKeyedData);
+        if (NS_FAILED(rv)) {
+          return rv;
+        }
+        // Append it to our list.
+        scalarsToReflect.AppendElement(mozilla::MakePair(info.name(), scalarKeyedData));
+      }
+    }
+
+    if (aClearScalars) {
+      // The map already takes care of freeing the allocated memory.
+      gKeyedScalarStorageMap.Clear();
+    }
+  }
+
+  // Reflect it to JS.
+  for (nsTArray<DataPair>::size_type i = 0; i < scalarsToReflect.Length(); i++) {
+    const DataPair& keyedScalarData = scalarsToReflect[i];
+
+    // Go through each keyed scalar and create a keyed scalar object.
+    // This object will hold the values for all the keyed scalar keys.
+    JS::RootedObject keyedScalarObj(aCx, JS_NewPlainObject(aCx));
+
+    // Define a property for each scalar key, then add it to the keyed scalar
+    // object.
+    const nsTArray<KeyedScalar::KeyValuePair>& keyProps = keyedScalarData.second();
+    for (uint32_t i = 0; i < keyProps.Length(); i++) {
+      const KeyedScalar::KeyValuePair& keyData = keyProps[i];
+
+      // Convert the value for the key to a JSValue.
+      JS::Rooted<JS::Value> keyJsValue(aCx);
+      nsresult rv =
+        nsContentUtils::XPConnect()->VariantToJS(aCx, keyedScalarObj, keyData.second(), &keyJsValue);
+      if (NS_FAILED(rv)) {
+        return rv;
+      }
+
+      // Add the key to the scalar representation.
+      const NS_ConvertUTF8toUTF16 key(keyData.first());
+      if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(), keyJsValue, JSPROP_ENUMERATE)) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+
+    // Add the scalar to the root object.
+    if (!JS_DefineProperty(aCx, root_obj, keyedScalarData.first(), keyedScalarObj, JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  return NS_OK;
+}
+
+/**
  * Resets all the stored scalars. This is intended to be only used in tests.
  */
 void
 TelemetryScalar::ClearScalars()
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
   gScalarStorageMap.Clear();
+  gKeyedScalarStorageMap.Clear();
 }
 
 size_t
 TelemetryScalar::GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf)
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
   return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
 }
 
 size_t
 TelemetryScalar::GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
 {
   StaticMutexAutoLock locker(gTelemetryScalarsMutex);
   size_t n = 0;
+  // For the plain scalars...
   for (auto iter = gScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
     ScalarBase* scalar = static_cast<ScalarBase*>(iter.Data());
     n += scalar->SizeOfIncludingThis(aMallocSizeOf);
   }
+  // ...and for the keyed scalars.
+  for (auto iter = gKeyedScalarStorageMap.Iter(); !iter.Done(); iter.Next()) {
+    KeyedScalar* scalar = static_cast<KeyedScalar*>(iter.Data());
+    n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+  }
   return n;
 }
--- a/toolkit/components/telemetry/TelemetryScalar.h
+++ b/toolkit/components/telemetry/TelemetryScalar.h
@@ -24,23 +24,40 @@ void SetCanRecordExtended(bool b);
 // JS API Endpoints.
 nsresult Add(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
 nsresult Set(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
 nsresult SetMaximum(const nsACString& aName, JS::HandleValue aVal, JSContext* aCx);
 nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars,
                          JSContext* aCx, uint8_t optional_argc,
                          JS::MutableHandle<JS::Value> aResult);
 
+// Keyed JS API Endpoints.
+nsresult Add(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+             JSContext* aCx);
+nsresult Set(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+             JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, const nsAString& aKey, JS::HandleValue aVal,
+                    JSContext* aCx);
+nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars,
+                              JSContext* aCx, uint8_t optional_argc,
+                              JS::MutableHandle<JS::Value> aResult);
+
 // C++ API Endpoints.
 void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
 void Set(mozilla::Telemetry::ScalarID aId, bool aValue);
 void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
 
+// Keyed C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
 // Only to be used for testing.
 void ClearScalars();
 
 size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf);
 size_t GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
 
 } // namespace TelemetryScalar
 
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -966,26 +966,35 @@ var Impl = {
           ret[suffix][id][key] = this.packHistogram(snapshot[key]);
         }
       }
     }
 
     return ret;
   },
 
-  getScalars: function (subsession, clearSubsession) {
-    this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " + clearSubsession);
+  /**
+   * Get a snapshot of the scalars and clear them.
+   * @param {subsession} If true, then we collect the data for a subsession.
+   * @param {clearSubsession} If true, we  need to clear the subsession.
+   * @param {keyed} Take a snapshot of keyed or non keyed scalars.
+   * @return {Object} The scalar data as a Javascript object.
+   */
+  getScalars: function (subsession, clearSubsession, keyed) {
+    this._log.trace("getScalars - subsession: " + subsession + ", clearSubsession: " +
+                    clearSubsession + ", keyed: " + keyed);
 
     if (!subsession) {
       // We only support scalars for subsessions.
       this._log.trace("getScalars - We only support scalars in subsessions.");
       return {};
     }
 
-    let scalarsSnapshot =
+    let scalarsSnapshot = keyed ?
+      Telemetry.snapshotKeyedScalars(this.getDatasetType(), clearSubsession) :
       Telemetry.snapshotScalars(this.getDatasetType(), clearSubsession);
 
     // Don't return the test scalars.
     let ret = {};
     for (let name in scalarsSnapshot) {
       if (name.startsWith('telemetry.test') && this._testing == false) {
         this._log.trace("getScalars - Skipping test scalar: " + name);
       } else {
@@ -1280,16 +1289,17 @@ var Impl = {
     // Additional payload for chrome process.
     let histograms = protect(() => this.getHistograms(isSubsession, clearSubsession), {});
     let keyedHistograms = protect(() => this.getKeyedHistograms(isSubsession, clearSubsession), {});
     payloadObj.histograms = histograms[HISTOGRAM_SUFFIXES.PARENT] || {};
     payloadObj.keyedHistograms = keyedHistograms[HISTOGRAM_SUFFIXES.PARENT] || {};
     payloadObj.processes = {
       parent: {
         scalars: protect(() => this.getScalars(isSubsession, clearSubsession)),
+        keyedScalars: protect(() => this.getScalars(isSubsession, clearSubsession, true)),
       },
       content: {
         histograms: histograms[HISTOGRAM_SUFFIXES.CONTENT],
         keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.CONTENT],
       },
     };
 
     payloadObj.info = info;
--- a/toolkit/components/telemetry/nsITelemetry.idl
+++ b/toolkit/components/telemetry/nsITelemetry.idl
@@ -388,16 +388,59 @@ interface nsITelemetry : nsISupports
    *
    * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
    * @param [aClear=false] Whether to clear out the scalars after snapshotting.
    */
   [implicit_jscontext, optional_argc]
   jsval snapshotScalars(in uint32_t aDataset, [optional] in boolean aClear);
 
   /**
+   * Adds the value to the given keyed scalar.
+   *
+   * @param aName The scalar name.
+   * @param aKey The key name.
+   * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+   */
+  [implicit_jscontext]
+  void keyedScalarAdd(in ACString aName, in AString aKey, in jsval aValue);
+
+  /**
+   * Sets the keyed scalar to the given value.
+   *
+   * @param aName The scalar name.
+   * @param aKey The key name.
+   * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+   *        type of the scalar, the function will fail.
+   */
+  [implicit_jscontext]
+  void keyedScalarSet(in ACString aName, in AString aKey, in jsval aValue);
+
+  /**
+   * Sets the keyed scalar to the maximum of the current and the passed value.
+   *
+   * @param aName The scalar name.
+   * @param aKey The key name.
+   * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+   */
+  [implicit_jscontext]
+  void keyedScalarSetMaximum(in ACString aName, in AString aKey, in jsval aValue);
+
+  /**
+   * Serializes the keyed scalars from the given dataset to a JSON-style object and
+   * resets them.
+   * The returned structure looks like:
+   *   { "group1.probe": { "key_1": 2, "key_2": 1, ... }, ... }
+   *
+   * @param aDataset DATASET_RELEASE_CHANNEL_OPTOUT or DATASET_RELEASE_CHANNEL_OPTIN.
+   * @param [aClear=false] Whether to clear out the scalars after snapshotting.
+   */
+  [implicit_jscontext, optional_argc]
+  jsval snapshotKeyedScalars(in uint32_t aDataset, [optional] in boolean aClear);
+
+  /**
    * Resets all the stored scalars. This is intended to be only used in tests.
    */
   void clearScalars();
 
   /**
    * Immediately sends any Telemetry batched on this process to the parent
    * process. This is intended only to be used on process shutdown.
    */
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
@@ -1,25 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/
 */
 
 const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
 const STRING_SCALAR = "telemetry.test.string_kind";
 const BOOLEAN_SCALAR = "telemetry.test.boolean_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
 
 add_task(function* test_serializationFormat() {
   Telemetry.clearScalars();
 
   // Set the scalars to a known value.
   const expectedUint = 3785;
   const expectedString = "some value";
   Telemetry.scalarSet(UINT_SCALAR, expectedUint);
   Telemetry.scalarSet(STRING_SCALAR, expectedString);
   Telemetry.scalarSet(BOOLEAN_SCALAR, true);
+  Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "first_key", 1234);
 
   // Get a snapshot of the scalars.
   const scalars =
     Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
 
   // Check that they are serialized to the correct format.
   Assert.equal(typeof(scalars[UINT_SCALAR]), "number",
                UINT_SCALAR + " must be serialized to the correct format.");
@@ -30,16 +32,52 @@ add_task(function* test_serializationFor
   Assert.equal(typeof(scalars[STRING_SCALAR]), "string",
                STRING_SCALAR + " must be serialized to the correct format.");
   Assert.equal(scalars[STRING_SCALAR], expectedString,
                STRING_SCALAR + " must have the correct value.");
   Assert.equal(typeof(scalars[BOOLEAN_SCALAR]), "boolean",
                BOOLEAN_SCALAR + " must be serialized to the correct format.");
   Assert.equal(scalars[BOOLEAN_SCALAR], true,
                BOOLEAN_SCALAR + " must have the correct value.");
+  Assert.ok(!(KEYED_UINT_SCALAR in scalars),
+            "Keyed scalars must be reported in a separate section.");
+});
+
+add_task(function* test_keyedSerializationFormat() {
+  Telemetry.clearScalars();
+
+  const expectedKey = "first_key";
+  const expectedOtherKey = "漢語";
+  const expectedUint = 3785;
+  const expectedOtherValue = 1107;
+
+  Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+  Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedKey, expectedUint);
+  Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedOtherKey, expectedOtherValue);
+
+  // Get a snapshot of the scalars.
+  const keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+  Assert.ok(!(UINT_SCALAR in keyedScalars),
+            UINT_SCALAR + " must not be serialized with the keyed scalars.");
+  Assert.ok(KEYED_UINT_SCALAR in keyedScalars,
+            KEYED_UINT_SCALAR + " must be serialized with the keyed scalars.");
+  Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 2,
+               "The keyed scalar must contain exactly 2 keys.");
+  Assert.ok(expectedKey in keyedScalars[KEYED_UINT_SCALAR],
+            KEYED_UINT_SCALAR + " must contain the expected keys.");
+  Assert.ok(expectedOtherKey in keyedScalars[KEYED_UINT_SCALAR],
+            KEYED_UINT_SCALAR + " must contain the expected keys.");
+  Assert.ok(Number.isInteger(keyedScalars[KEYED_UINT_SCALAR][expectedKey]),
+               KEYED_UINT_SCALAR + "." + expectedKey + " must be a finite integer.");
+  Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedKey], expectedUint,
+               KEYED_UINT_SCALAR + "." + expectedKey + " must have the correct value.");
+  Assert.equal(keyedScalars[KEYED_UINT_SCALAR][expectedOtherKey], expectedOtherValue,
+               KEYED_UINT_SCALAR + "." + expectedOtherKey + " must have the correct value.");
 });
 
 add_task(function* test_nonexistingScalar() {
   const NON_EXISTING_SCALAR = "telemetry.test.non_existing";
 
   Telemetry.clearScalars();
 
   // Make sure we throw on any operation for non-existing scalars.
@@ -48,46 +86,71 @@ add_task(function* test_nonexistingScala
                 "Adding to a non existing scalar must throw.");
   Assert.throws(() => Telemetry.scalarSet(NON_EXISTING_SCALAR, 11715),
                 /NS_ERROR_ILLEGAL_VALUE/,
                 "Setting a non existing scalar must throw.");
   Assert.throws(() => Telemetry.scalarSetMaximum(NON_EXISTING_SCALAR, 11715),
                 /NS_ERROR_ILLEGAL_VALUE/,
                 "Setting the maximum of a non existing scalar must throw.");
 
+  // Make sure we throw on any operation for non-existing scalars.
+  Assert.throws(() => Telemetry.keyedScalarAdd(NON_EXISTING_SCALAR, "some_key", 11715),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Adding to a non existing keyed scalar must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSet(NON_EXISTING_SCALAR, "some_key", 11715),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Setting a non existing keyed scalar must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSetMaximum(NON_EXISTING_SCALAR, "some_key", 11715),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Setting the maximum of a non keyed existing scalar must throw.");
+
   // Get a snapshot of the scalars.
   const scalars =
     Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
 
   Assert.ok(!(NON_EXISTING_SCALAR in scalars), "The non existing scalar must not be persisted.");
+
+  const keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+  Assert.ok(!(NON_EXISTING_SCALAR in keyedScalars),
+            "The non existing keyed scalar must not be persisted.");
 });
 
 add_task(function* test_expiredScalar() {
   const EXPIRED_SCALAR = "telemetry.test.expired";
+  const EXPIRED_KEYED_SCALAR = "telemetry.test.keyed_expired";
   const UNEXPIRED_SCALAR = "telemetry.test.unexpired";
 
   Telemetry.clearScalars();
 
   // Try to set the expired scalar to some value. We will not be recording the value,
   // but we shouldn't throw.
   Telemetry.scalarAdd(EXPIRED_SCALAR, 11715);
   Telemetry.scalarSet(EXPIRED_SCALAR, 11715);
   Telemetry.scalarSetMaximum(EXPIRED_SCALAR, 11715);
+  Telemetry.keyedScalarAdd(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+  Telemetry.keyedScalarSet(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+  Telemetry.keyedScalarSetMaximum(EXPIRED_KEYED_SCALAR, "some_key", 11715);
 
   // The unexpired scalar has an expiration version, but far away in the future.
   const expectedValue = 11716;
   Telemetry.scalarSet(UNEXPIRED_SCALAR, expectedValue);
 
   // Get a snapshot of the scalars.
   const scalars =
     Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+  const keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
 
   Assert.ok(!(EXPIRED_SCALAR in scalars), "The expired scalar must not be persisted.");
   Assert.equal(scalars[UNEXPIRED_SCALAR], expectedValue,
                "The unexpired scalar must be persisted with the correct value.");
+  Assert.ok(!(EXPIRED_KEYED_SCALAR in keyedScalars),
+            "The expired keyed scalar must not be persisted.");
 });
 
 add_task(function* test_unsignedIntScalar() {
   let checkScalar = (expectedValue) => {
     const scalars =
       Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
     Assert.equal(scalars[UINT_SCALAR], expectedValue,
                  UINT_SCALAR + " must contain the expected value.");
@@ -267,34 +330,245 @@ add_task(function* test_scalarRecording(
   // Check that both opt-out and opt-in scalars are recorded.
   Telemetry.canRecordExtended = true;
   Telemetry.scalarSet(OPTOUT_SCALAR, 5);
   Telemetry.scalarSet(OPTIN_SCALAR, 6);
   checkValue(OPTOUT_SCALAR, 5);
   checkValue(OPTIN_SCALAR, 6);
 });
 
+add_task(function* test_keyedScalarRecording() {
+  const OPTIN_SCALAR = "telemetry.test.keyed_release_optin";
+  const OPTOUT_SCALAR = "telemetry.test.keyed_release_optout";
+  const testKey = "policy_key";
+
+  let checkValue = (scalarName, expectedValue) => {
+    const scalars =
+      Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+    Assert.equal(scalars[scalarName][testKey], expectedValue,
+                 scalarName + " must contain the expected value.");
+  };
+
+  let checkNotSerialized = (scalarName) => {
+    const scalars =
+      Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+    Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+  };
+
+  Telemetry.canRecordBase = false;
+  Telemetry.canRecordExtended = false;
+  Telemetry.clearScalars();
+
+  // Check that no scalar is recorded if both base and extended recording are off.
+  Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+  Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+  checkNotSerialized(OPTOUT_SCALAR);
+  checkNotSerialized(OPTIN_SCALAR);
+
+  // Check that opt-out scalars are recorded, while opt-in are not.
+  Telemetry.canRecordBase = true;
+  Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+  Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+  checkValue(OPTOUT_SCALAR, 3);
+  checkNotSerialized(OPTIN_SCALAR);
+
+  // Check that both opt-out and opt-in scalars are recorded.
+  Telemetry.canRecordExtended = true;
+  Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 5);
+  Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 6);
+  checkValue(OPTOUT_SCALAR, 5);
+  checkValue(OPTIN_SCALAR, 6);
+});
+
 add_task(function* test_subsession() {
   Telemetry.clearScalars();
 
   // Set the scalars to a known value.
   Telemetry.scalarSet(UINT_SCALAR, 3785);
   Telemetry.scalarSet(STRING_SCALAR, "some value");
   Telemetry.scalarSet(BOOLEAN_SCALAR, false);
+  Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "some_random_key", 12);
 
   // Get a snapshot and reset the subsession. The value we set must be there.
   let scalars =
     Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+  let keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
   Assert.equal(scalars[UINT_SCALAR], 3785,
                UINT_SCALAR + " must contain the expected value.");
   Assert.equal(scalars[STRING_SCALAR], "some value",
                STRING_SCALAR + " must contain the expected value.");
   Assert.equal(scalars[BOOLEAN_SCALAR], false,
                BOOLEAN_SCALAR + " must contain the expected value.");
+  Assert.equal(keyedScalars[KEYED_UINT_SCALAR]["some_random_key"], 12,
+               KEYED_UINT_SCALAR + " must contain the expected value.");
 
   // Get a new snapshot and reset the subsession again. Since no new value
   // was set, the scalars should not be reported.
   scalars =
     Telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+  keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+
   Assert.ok(!(UINT_SCALAR in scalars), UINT_SCALAR + " must be empty and not reported.");
   Assert.ok(!(STRING_SCALAR in scalars), STRING_SCALAR + " must be empty and not reported.");
   Assert.ok(!(BOOLEAN_SCALAR in scalars), BOOLEAN_SCALAR + " must be empty and not reported.");
+  Assert.ok(!(KEYED_UINT_SCALAR in keyedScalars), KEYED_UINT_SCALAR + " must be empty and not reported.");
 });
+
+add_task(function* test_keyed_uint() {
+  Telemetry.clearScalars();
+
+  const KEYS = [ "a_key", "another_key", "third_key" ];
+  let expectedValues = [ 1, 1, 1 ];
+
+  // Set all the keys to a baseline value.
+  for (let key of KEYS) {
+    Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, key, 1);
+  }
+
+  // Increment only one key.
+  Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, KEYS[1], 1);
+  expectedValues[1]++;
+
+  // Use SetMaximum on the third key.
+  Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, KEYS[2], 37);
+  expectedValues[2] = 37;
+
+  // Get a snapshot of the scalars and make sure the keys contain
+  // the correct values.
+  const keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+  for (let k = 0; k < 3; k++) {
+    const keyName = KEYS[k];
+    Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValues[k],
+                 KEYED_UINT_SCALAR + "." + keyName + " must contain the correct value.");
+  }
+
+  // Are we still throwing when doing unsupported things on uint keyed scalars?
+  // Just test one single unsupported operation, the other are covered in the plain
+  // unsigned scalar test.
+  Assert.throws(() => Telemetry.scalarSet(KEYED_UINT_SCALAR, "new_key", "unexpected value"),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Setting the scalar to an unexpected value type must throw.");
+});
+
+add_task(function* test_keyed_boolean() {
+  Telemetry.clearScalars();
+
+  const KEYED_BOOLEAN_TYPE = "telemetry.test.keyed_boolean_kind";
+  const first_key = "first_key";
+  const second_key = "second_key";
+
+  // Set the initial values.
+  Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, true);
+  Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, false);
+
+  // Get a snapshot of the scalars and make sure the keys contain
+  // the correct values.
+  let keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+  Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], true,
+               "The key must contain the expected value.");
+  Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], false,
+               "The key must contain the expected value.");
+
+  // Now flip the values and make sure we get the expected values back.
+  Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, false);
+  Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, true);
+
+  keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+  Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][first_key], false,
+               "The key must contain the expected value.");
+  Assert.equal(keyedScalars[KEYED_BOOLEAN_TYPE][second_key], true,
+               "The key must contain the expected value.");
+
+  // Are we still throwing when doing unsupported things on a boolean keyed scalars?
+  // Just test one single unsupported operation, the other are covered in the plain
+  // boolean scalar test.
+  Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_BOOLEAN_TYPE, "somehey", 1),
+                /NS_ERROR_NOT_AVAILABLE/,
+                "Using an unsupported operation must throw.");
+});
+
+add_task(function* test_keyed_keys_length() {
+  Telemetry.clearScalars();
+
+  const LONG_KEY_STRING =
+    "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv.somemoresowereach70chars";
+  const NORMAL_KEY = "a_key";
+
+  // Set the value for a key within the length limits.
+  Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, NORMAL_KEY, 1);
+
+  // Now try to set and modify the value for a very long key.
+  Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Using keys longer than 70 characters must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LONG_KEY_STRING, 1),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Using keys longer than 70 characters must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10),
+                /NS_ERROR_ILLEGAL_VALUE/,
+                "Using keys longer than 70 characters must throw.");
+
+  // Make sure the key with the right length contains the expected value.
+  let keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+  Assert.equal(Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length, 1,
+               "The keyed scalar must contain exactly 1 key.");
+  Assert.ok(NORMAL_KEY in keyedScalars[KEYED_UINT_SCALAR],
+            "The keyed scalar must contain the expected key.");
+  Assert.equal(keyedScalars[KEYED_UINT_SCALAR][NORMAL_KEY], 1,
+               "The key must contain the expected value.");
+  Assert.ok(!(LONG_KEY_STRING in keyedScalars[KEYED_UINT_SCALAR]),
+            "The data for the long key should not have been recorded.");
+});
+
+add_task(function* test_keyed_max_keys() {
+  Telemetry.clearScalars();
+
+  // Generate the names for the first 100 keys.
+  let keyNamesSet = new Set();
+  for (let k = 0; k < 100; k++) {
+    keyNamesSet.add("key_" + k);
+  }
+
+  // Add 100 keys to an histogram and set their initial value.
+  let valueToSet = 0;
+  keyNamesSet.forEach(keyName => {
+    Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, keyName, valueToSet++);
+  });
+
+  // Perform some operations on the 101th key. This should throw, as
+  // we're not allowed to have more than 100 keys.
+  const LAST_KEY_NAME = "overflowing_key";
+  Assert.throws(() => Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10),
+                /NS_ERROR_FAILURE/,
+                "Using more than 100 keys must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LAST_KEY_NAME, 1),
+                /NS_ERROR_FAILURE/,
+                "Using more than 100 keys must throw.");
+  Assert.throws(() => Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10),
+                /NS_ERROR_FAILURE/,
+                "Using more than 100 keys must throw.");
+
+  // Make sure all the keys except the last one are available and have the correct
+  // values.
+  let keyedScalars =
+    Telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN);
+
+  // Check that the keyed scalar only contain the first 100 keys.
+  const reportedKeysSet = new Set(Object.keys(keyedScalars[KEYED_UINT_SCALAR]));
+  Assert.ok([...keyNamesSet].filter(x => reportedKeysSet.has(x)) &&
+            [...reportedKeysSet].filter(x => keyNamesSet.has(x)),
+            "The keyed scalar must contain all the 100 keys, and drop the others.");
+
+  // Check that all the keys recorded the expected values.
+  let expectedValue = 0;
+  keyNamesSet.forEach(keyName => {
+    Assert.equal(keyedScalars[KEYED_UINT_SCALAR][keyName], expectedValue++,
+                 "The key must contain the expected value.");
+  });
+});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -239,33 +239,59 @@ function checkPayloadInfo(data) {
   Assert.ok(data.timezoneOffset >= -12*60, "The timezone must be in a valid range.");
   Assert.ok(data.timezoneOffset <= 12*60, "The timezone must be in a valid range.");
 }
 
 function checkScalars(processes) {
   // Check that the scalars section is available in the ping payload.
   const parentProcess = processes.parent;
   Assert.ok("scalars" in parentProcess, "The scalars section must be available in the parent process.");
+  Assert.ok("keyedScalars" in parentProcess, "The keyedScalars section must be available in the parent process.");
   Assert.equal(typeof parentProcess.scalars, "object", "The scalars entry must be an object.");
+  Assert.equal(typeof parentProcess.keyedScalars, "object", "The keyedScalars entry must be an object.");
+
+  let checkScalar = function(scalar) {
+    // Check if the value is of a supported type.
+    const valueType = typeof(scalar);
+    switch (valueType) {
+      case "string":
+        Assert.ok(scalar.length <= 50,
+                  "String values can't have more than 50 characters");
+      break;
+      case "number":
+        Assert.ok(scalar >= 0,
+                  "We only support unsigned integer values in scalars.");
+      break;
+      case "boolean":
+        Assert.ok(true,
+                  "Boolean scalar found.");
+      break;
+      default:
+        Assert.ok(false,
+                  name + " contains an unsupported value type (" + valueType + ")");
+    }
+  }
 
   // Check that we have valid scalar entries.
   const scalars = parentProcess.scalars;
   for (let name in scalars) {
     Assert.equal(typeof name, "string", "Scalar names must be strings.");
-    // Check if the value is of a supported type.
-    const valueType = typeof(scalars[name]);
-    if (valueType === "string") {
-      Assert.ok(scalars[name].length <= 50,
-                "String values can't have more than 50 characters");
-    } else if (valueType === "number") {
-      Assert.ok(scalars[name] >= 0,
-                "We only support unsigned integer values in scalars.");
-    } else {
-      Assert.ok(false,
-                name + " contains an unsupported value type (" + valueType + ")");
+    checkScalar(scalar[name]);
+  }
+
+  // Check that we have valid keyed scalar entries.
+  const keyedScalars = parentProcess.keyedScalars;
+  for (let name in keyedScalars) {
+    Assert.equal(typeof name, "string", "Scalar names must be strings.");
+    Assert.ok(Object.keys(keyedScalars[name]).length,
+              "The reported keyed scalars must contain at least 1 key.");
+    for (let key in keyedScalars[name]) {
+      Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
+      Assert.ok(key.length <= 70, "Keyed scalar keys can't have more than 70 characters.");
+      checkScalar(scalar[name][key]);
     }
   }
 }
 
 function checkPayload(payload, reason, successfulPings, savedPings) {
   Assert.ok("info" in payload, "Payload must contain an info section.");
   checkPayloadInfo(payload.info);