/* -*- 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(std::tolower(*start))); } else { snake.Append(*start); } } return snake; } NS_IMETHODIMP GleanEvent::Record(JS::Handle 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 extraKeys; nsTArray extraValues; CopyableTArray telExtras; JS::Rooted obj(aCx, &aExtra.toObject()); JS::Rooted 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 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 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 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 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 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 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 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 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