import { CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE, } from "common/Actions.sys.mjs"; import Joi from "joi-browser"; export const baseKeys = { // client_id will be set by PingCentre if it doesn't exist. client_id: Joi.string().optional(), addon_version: Joi.string().required(), locale: Joi.string().required(), session_id: Joi.string(), page: Joi.valid([ "about:home", "about:newtab", "about:welcome", "both", "unknown", ]), user_prefs: Joi.number().integer().required(), }; export const BasePing = Joi.object() .keys(baseKeys) .options({ allowUnknown: true }); export const eventsTelemetryExtraKeys = Joi.object() .keys({ session_id: baseKeys.session_id.required(), page: baseKeys.page.required(), addon_version: baseKeys.addon_version.required(), user_prefs: baseKeys.user_prefs.required(), action_position: Joi.string().optional(), }) .options({ allowUnknown: false }); export const UserEventPing = Joi.object().keys( Object.assign({}, baseKeys, { session_id: baseKeys.session_id.required(), page: baseKeys.page.required(), source: Joi.string(), event: Joi.string().required(), action: Joi.valid("activity_stream_user_event").required(), metadata_source: Joi.string(), highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]), recommender_type: Joi.string(), value: Joi.object().keys({ newtab_url_category: Joi.string(), newtab_extension_id: Joi.string(), home_url_category: Joi.string(), home_extension_id: Joi.string(), }), }) ); export const UTUserEventPing = Joi.array().items( Joi.string().required().valid("activity_stream"), Joi.string().required().valid("event"), Joi.string() .required() .valid([ "CLICK", "SEARCH", "BLOCK", "DELETE", "DELETE_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_NEWTAB_PREFS", "CLOSE_NEWTAB_PREFS", "BOOKMARK_DELETE", "BOOKMARK_ADD", "PIN", "UNPIN", "SAVE_TO_POCKET", ]), Joi.string().required(), eventsTelemetryExtraKeys ); // Use this to validate actions generated from Redux export const UserEventAction = Joi.object().keys({ type: Joi.string().required(), data: Joi.object() .keys({ event: Joi.valid([ "CLICK", "SEARCH", "SEARCH_HANDOFF", "BLOCK", "DELETE", "DELETE_CONFIRM", "DIALOG_CANCEL", "DIALOG_OPEN", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_NEWTAB_PREFS", "CLOSE_NEWTAB_PREFS", "BOOKMARK_DELETE", "BOOKMARK_ADD", "PIN", "PREVIEW_REQUEST", "UNPIN", "SAVE_TO_POCKET", "MENU_MOVE_UP", "MENU_MOVE_DOWN", "SCREENSHOT_REQUEST", "MENU_REMOVE", "MENU_COLLAPSE", "MENU_EXPAND", "MENU_MANAGE", "MENU_ADD_TOPSITE", "MENU_PRIVACY_NOTICE", "DELETE_FROM_POCKET", "ARCHIVE_FROM_POCKET", "SKIPPED_SIGNIN", "SUBMIT_EMAIL", "SUBMIT_SIGNIN", "SHOW_PRIVACY_INFO", "CLICK_PRIVACY_INFO", ]).required(), source: Joi.valid(["TOP_SITES", "TOP_STORIES", "HIGHLIGHTS"]), action_position: Joi.number().integer(), value: Joi.object().keys({ icon_type: Joi.valid([ "tippytop", "rich_icon", "screenshot_with_icon", "screenshot", "no_image", "custom_screenshot", ]), card_type: Joi.valid([ "bookmark", "trending", "pinned", "pocket", "search", "spoc", "organic", ]), search_vendor: Joi.valid(["google", "amazon"]), has_flow_params: Joi.bool(), }), }) .required(), meta: Joi.object() .keys({ to: Joi.valid(MAIN_MESSAGE_TYPE).required(), from: Joi.valid(CONTENT_MESSAGE_TYPE).required(), }) .required(), }); export const TileSchema = Joi.object().keys({ id: Joi.number().integer().required(), pos: Joi.number().integer(), }); export const ImpressionStatsPing = Joi.object().keys( Object.assign({}, baseKeys, { source: Joi.string().required(), impression_id: Joi.string().required(), tiles: Joi.array().items(TileSchema).required(), click: Joi.number().integer(), block: Joi.number().integer(), pocket: Joi.number().integer(), }) ); export const SessionPing = Joi.object().keys( Object.assign({}, baseKeys, { session_id: baseKeys.session_id.required(), page: baseKeys.page.required(), session_duration: Joi.number().integer(), action: Joi.valid("activity_stream_session").required(), profile_creation_date: Joi.number().integer(), perf: Joi.object() .keys({ // How long it took in ms for data to be ready for display. highlights_data_late_by_ms: Joi.number().positive(), // Timestamp of the action perceived by the user to trigger the load // of this page. // // Not required at least for the error cases where the // observer event doesn't fire load_trigger_ts: Joi.number() .integer() .notes(["server counter", "server counter alert"]), // What was the perceived trigger of the load action? // // Not required at least for the error cases where the observer event // doesn't fire load_trigger_type: Joi.valid([ "first_window_opened", "menu_plus_or_keyboard", "unexpected", ]) .notes(["server counter", "server counter alert"]) .required(), // How long it took in ms for data to be ready for display. topsites_data_late_by_ms: Joi.number().positive(), // When did the topsites element finish painting? Note that, at least for // the first tab to be loaded, and maybe some others, this will be before // topsites has yet to receive screenshots updates from the add-on code, // and is therefore just showing placeholder screenshots. topsites_first_painted_ts: Joi.number() .integer() .notes(["server counter", "server counter alert"]), // Information about the quality of TopSites images and icons. topsites_icon_stats: Joi.object().keys({ custom_screenshot: Joi.number(), rich_icon: Joi.number(), screenshot: Joi.number(), screenshot_with_icon: Joi.number(), tippytop: Joi.number(), no_image: Joi.number(), }), // The count of pinned Top Sites. topsites_pinned: Joi.number(), // The count of search shortcut Top Sites. topsites_search_shortcuts: Joi.number(), // When the page itself receives an event that document.visibilityState // == visible. // // Not required at least for the (error?) case where the // visibility_event doesn't fire. (It's not clear whether this // can happen in practice, but if it does, we'd like to know about it). visibility_event_rcvd_ts: Joi.number() .integer() .notes(["server counter", "server counter alert"]), // The boolean to signify whether the page is preloaded or not. is_preloaded: Joi.bool().required(), }) .required(), }) ); export const ASRouterEventPing = Joi.object() .keys({ addon_version: Joi.string().required(), locale: Joi.string().required(), message_id: Joi.string().required(), event: Joi.string().required(), client_id: Joi.string(), impression_id: Joi.string(), }) .or("client_id", "impression_id"); export const UTSessionPing = Joi.array().items( Joi.string().required().valid("activity_stream"), Joi.string().required().valid("end"), Joi.string().required().valid("session"), Joi.string().required(), eventsTelemetryExtraKeys ); export function chaiAssertions(_chai, utils) { const { Assertion } = _chai; Assertion.addMethod("validate", function (schema, schemaName) { const { error } = Joi.validate(this._obj, schema, { allowUnknown: false }); this.assert( !error, `Expected to be ${ schemaName ? `a valid ${schemaName}` : "valid" } but there were errors: ${error}` ); }); const assertions = { /** * assert.validate - Validates an item given a Joi schema * * @param {any} actual The item to validate * @param {obj} schema A Joi schema */ validate(actual, schema, schemaName) { new Assertion(actual).validate(schema, schemaName); }, /** * isUserEventAction - Passes if the item is a valid UserEvent action * * @param {any} actual The item to validate */ isUserEventAction(actual) { new Assertion(actual).validate(UserEventAction, "UserEventAction"); }, }; Object.assign(_chai.assert, assertions); }