diff options
Diffstat (limited to 'toolkit/components/glean/bindings/private/Event.cpp')
-rw-r--r-- | toolkit/components/glean/bindings/private/Event.cpp | 206 |
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 |