summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/bindings/private/Event.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/glean/bindings/private/Event.cpp')
-rw-r--r--toolkit/components/glean/bindings/private/Event.cpp206
1 files changed, 206 insertions, 0 deletions
diff --git a/toolkit/components/glean/bindings/private/Event.cpp b/toolkit/components/glean/bindings/private/Event.cpp
new file mode 100644
index 0000000000..d211c91856
--- /dev/null
+++ b/toolkit/components/glean/bindings/private/Event.cpp
@@ -0,0 +1,206 @@
+/* -*- 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/Event.h"
+
+#include "Common.h"
+#include "nsString.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsIClassInfoImpl.h"
+#include "jsapi.h"
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetProperty, JS_GetPropertyById
+#include "nsIScriptError.h"
+
+namespace mozilla::glean {
+
+NS_IMPL_CLASSINFO(GleanEvent, nullptr, 0, {0})
+NS_IMPL_ISUPPORTS_CI(GleanEvent, nsIGleanEvent)
+
+// Convert all capital letters to "_x" where "x" is the corresponding lowercase.
+nsCString camelToSnake(const nsACString& aCamel) {
+ nsCString snake;
+ const auto* start = aCamel.BeginReading();
+ const auto* end = aCamel.EndReading();
+ for (; start != end; ++start) {
+ if ('A' <= *start && *start <= 'Z') {
+ snake.AppendLiteral("_");
+ snake.Append(static_cast<char>(std::tolower(*start)));
+ } else {
+ snake.Append(*start);
+ }
+ }
+ return snake;
+}
+
+NS_IMETHODIMP
+GleanEvent::Record(JS::Handle<JS::Value> aExtra, JSContext* aCx) {
+ if (aExtra.isNullOrUndefined()) {
+ mEvent.Record();
+ return NS_OK;
+ }
+
+ if (!aExtra.isObject()) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Extras need to be an object. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+
+ nsTArray<nsCString> extraKeys;
+ nsTArray<nsCString> extraValues;
+ CopyableTArray<Telemetry::EventExtraEntry> telExtras;
+
+ JS::Rooted<JSObject*> obj(aCx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, obj, &ids)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Failed to enumerate object. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSCString jsKey;
+ if (!jsKey.init(aCx, ids[i])) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Extra dictionary should only contain string keys. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+
+ // We accept camelCase extra keys, but Glean requires snake_case.
+ auto snakeKey = camelToSnake(jsKey);
+
+ JS::Rooted<JS::Value> value(aCx);
+ if (!JS_GetPropertyById(aCx, obj, ids[i], &value)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Failed to get extra property. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+
+ nsAutoJSCString jsValue;
+ if (value.isString() || (value.isInt32() && value.toInt32() >= 0) ||
+ value.isBoolean()) {
+ if (!jsValue.init(aCx, value)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Can't extract extra property. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+ } else if (value.isNullOrUndefined()) {
+ // The extra key is present, but has an empty value.
+ // Treat as though it weren't here at all.
+ continue;
+ } else {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ u"Extra properties should have string, bool or non-negative integer values. Event will not be recorded."_ns);
+ return NS_OK;
+ }
+
+ extraKeys.AppendElement(snakeKey);
+ extraValues.AppendElement(jsValue);
+ telExtras.EmplaceBack(Telemetry::EventExtraEntry{jsKey, jsValue});
+ }
+
+ // Since this calls the implementation directly, we need to implement GIFFT
+ // here as well as in EventMetric::Record.
+ auto id = EventIdForMetric(mEvent.mId);
+ if (id) {
+ Telemetry::RecordEvent(id.extract(), Nothing(),
+ telExtras.IsEmpty() ? Nothing() : Some(telExtras));
+ }
+
+ // Calling the implementation directly, because we have a `string->string`
+ // map, not a `T->string` map the C++ API expects.
+ impl::fog_event_record(mEvent.mId, &extraKeys, &extraValues);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GleanEvent::TestGetValue(const nsACString& aStorageName, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ auto resEvents = mEvent.TestGetValue(aStorageName);
+ if (resEvents.isErr()) {
+ aResult.set(JS::UndefinedValue());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(resEvents.unwrapErr()));
+ return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA;
+ }
+ auto optEvents = resEvents.unwrap();
+ if (optEvents.isNothing()) {
+ aResult.set(JS::UndefinedValue());
+ return NS_OK;
+ }
+
+ auto events = optEvents.extract();
+
+ auto count = events.Length();
+ JS::Rooted<JSObject*> eventArray(aCx, JS::NewArrayObject(aCx, count));
+ if (NS_WARN_IF(!eventArray)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ auto* value = &events[i];
+
+ JS::Rooted<JSObject*> eventObj(aCx, JS_NewPlainObject(aCx));
+ if (NS_WARN_IF(!eventObj)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, eventObj, "timestamp",
+ (double)value->mTimestamp, JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define timestamp for event object.");
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> catStr(aCx);
+ if (!dom::ToJSValue(aCx, value->mCategory, &catStr) ||
+ !JS_DefineProperty(aCx, eventObj, "category", catStr,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define category for event object.");
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JS::Value> nameStr(aCx);
+ if (!dom::ToJSValue(aCx, value->mName, &nameStr) ||
+ !JS_DefineProperty(aCx, eventObj, "name", nameStr, JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define name for event object.");
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> extraObj(aCx, JS_NewPlainObject(aCx));
+ if (!JS_DefineProperty(aCx, eventObj, "extra", extraObj,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define extra for event object.");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (auto pair : value->mExtra) {
+ auto key = std::get<0>(pair);
+ auto val = std::get<1>(pair);
+ JS::Rooted<JS::Value> valStr(aCx);
+ if (!dom::ToJSValue(aCx, val, &valStr) ||
+ !JS_DefineProperty(aCx, extraObj, key.Data(), valStr,
+ JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define extra property for event object.");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!JS_DefineElement(aCx, eventArray, i, eventObj, JSPROP_ENUMERATE)) {
+ NS_WARNING("Failed to define item in events array.");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aResult.setObject(*eventArray);
+ return NS_OK;
+}
+
+} // namespace mozilla::glean