pings.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import {CONTENT_MESSAGE_TYPE, MAIN_MESSAGE_TYPE} from "common/Actions.jsm";
  2. import Joi from "joi-browser";
  3. export const baseKeys = {
  4. // client_id will be set by PingCentre if it doesn't exist.
  5. client_id: Joi.string().optional(),
  6. addon_version: Joi.string().required(),
  7. locale: Joi.string().required(),
  8. session_id: Joi.string(),
  9. page: Joi.valid(["about:home", "about:newtab", "about:welcome", "both", "unknown"]),
  10. user_prefs: Joi.number().integer().required(),
  11. };
  12. export const BasePing = Joi.object().keys(baseKeys).options({allowUnknown: true});
  13. export const eventsTelemetryExtraKeys = Joi.object().keys({
  14. session_id: baseKeys.session_id.required(),
  15. page: baseKeys.page.required(),
  16. addon_version: baseKeys.addon_version.required(),
  17. user_prefs: baseKeys.user_prefs.required(),
  18. action_position: Joi.string().optional(),
  19. }).options({allowUnknown: false});
  20. export const UserEventPing = Joi.object().keys(Object.assign({}, baseKeys, {
  21. session_id: baseKeys.session_id.required(),
  22. page: baseKeys.page.required(),
  23. source: Joi.string(),
  24. event: Joi.string().required(),
  25. action: Joi.valid("activity_stream_user_event").required(),
  26. metadata_source: Joi.string(),
  27. highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
  28. recommender_type: Joi.string(),
  29. value: Joi.object().keys({
  30. newtab_url_category: Joi.string(),
  31. newtab_extension_id: Joi.string(),
  32. home_url_category: Joi.string(),
  33. home_extension_id: Joi.string(),
  34. }),
  35. }));
  36. export const UTUserEventPing = Joi.array().items(
  37. Joi.string().required().valid("activity_stream"),
  38. Joi.string().required().valid("event"),
  39. Joi.string().required().valid([
  40. "CLICK",
  41. "SEARCH",
  42. "BLOCK",
  43. "DELETE",
  44. "DELETE_CONFIRM",
  45. "DIALOG_CANCEL",
  46. "DIALOG_OPEN",
  47. "OPEN_NEW_WINDOW",
  48. "OPEN_PRIVATE_WINDOW",
  49. "OPEN_NEWTAB_PREFS",
  50. "CLOSE_NEWTAB_PREFS",
  51. "BOOKMARK_DELETE",
  52. "BOOKMARK_ADD",
  53. "PIN",
  54. "UNPIN",
  55. "SAVE_TO_POCKET",
  56. ]),
  57. Joi.string().required(),
  58. eventsTelemetryExtraKeys
  59. );
  60. // Use this to validate actions generated from Redux
  61. export const UserEventAction = Joi.object().keys({
  62. type: Joi.string().required(),
  63. data: Joi.object().keys({
  64. event: Joi.valid([
  65. "CLICK",
  66. "SEARCH",
  67. "SEARCH_HANDOFF",
  68. "BLOCK",
  69. "DELETE",
  70. "DELETE_CONFIRM",
  71. "DIALOG_CANCEL",
  72. "DIALOG_OPEN",
  73. "OPEN_NEW_WINDOW",
  74. "OPEN_PRIVATE_WINDOW",
  75. "OPEN_NEWTAB_PREFS",
  76. "CLOSE_NEWTAB_PREFS",
  77. "BOOKMARK_DELETE",
  78. "BOOKMARK_ADD",
  79. "PIN",
  80. "PREVIEW_REQUEST",
  81. "UNPIN",
  82. "SAVE_TO_POCKET",
  83. "MENU_MOVE_UP",
  84. "MENU_MOVE_DOWN",
  85. "SCREENSHOT_REQUEST",
  86. "MENU_REMOVE",
  87. "MENU_COLLAPSE",
  88. "MENU_EXPAND",
  89. "MENU_MANAGE",
  90. "MENU_ADD_TOPSITE",
  91. "MENU_PRIVACY_NOTICE",
  92. "DELETE_FROM_POCKET",
  93. "ARCHIVE_FROM_POCKET",
  94. "SKIPPED_SIGNIN",
  95. "SUBMIT_EMAIL",
  96. ]).required(),
  97. source: Joi.valid(["TOP_SITES", "TOP_STORIES", "HIGHLIGHTS"]),
  98. action_position: Joi.number().integer(),
  99. value: Joi.object().keys({
  100. icon_type: Joi.valid(["tippytop", "rich_icon", "screenshot_with_icon", "screenshot", "no_image"]),
  101. card_type: Joi.valid(["bookmark", "trending", "pinned", "pocket", "search"]),
  102. search_vendor: Joi.valid(["google", "amazon"]),
  103. has_flow_params: Joi.bool(),
  104. }),
  105. }).required(),
  106. meta: Joi.object().keys({
  107. to: Joi.valid(MAIN_MESSAGE_TYPE).required(),
  108. from: Joi.valid(CONTENT_MESSAGE_TYPE).required(),
  109. }).required(),
  110. });
  111. export const UndesiredPing = Joi.object().keys(Object.assign({}, baseKeys, {
  112. source: Joi.string().required(),
  113. event: Joi.string().required(),
  114. action: Joi.valid("activity_stream_undesired_event").required(),
  115. value: Joi.number().required(),
  116. }));
  117. export const TileSchema = Joi.object().keys({
  118. id: Joi.number().integer().required(),
  119. pos: Joi.number().integer(),
  120. });
  121. export const ImpressionStatsPing = Joi.object().keys(Object.assign({}, baseKeys, {
  122. source: Joi.string().required(),
  123. impression_id: Joi.string().required(),
  124. client_id: Joi.valid("n/a").required(),
  125. session_id: Joi.valid("n/a").required(),
  126. action: Joi.valid("activity_stream_impression_stats").required(),
  127. tiles: Joi.array().items(TileSchema).required(),
  128. click: Joi.number().integer(),
  129. block: Joi.number().integer(),
  130. pocket: Joi.number().integer(),
  131. }));
  132. export const SpocsFillEntrySchema = Joi.object().keys({
  133. id: Joi.number().integer().required(),
  134. displayed: Joi.number().integer().required(),
  135. reason: Joi.string().required(),
  136. full_recalc: Joi.number().integer().required(),
  137. });
  138. export const SpocsFillPing = Joi.object().keys(Object.assign({}, baseKeys, {
  139. impression_id: Joi.string().required(),
  140. client_id: Joi.valid("n/a").required(),
  141. session_id: Joi.valid("n/a").required(),
  142. spoc_fills: Joi.array().items(SpocsFillEntrySchema).required(),
  143. }));
  144. export const PerfPing = Joi.object().keys(Object.assign({}, baseKeys, {
  145. source: Joi.string(),
  146. event: Joi.string().required(),
  147. action: Joi.valid("activity_stream_performance_event").required(),
  148. value: Joi.number().required(),
  149. }));
  150. export const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
  151. session_id: baseKeys.session_id.required(),
  152. page: baseKeys.page.required(),
  153. session_duration: Joi.number().integer(),
  154. action: Joi.valid("activity_stream_session").required(),
  155. perf: Joi.object().keys({
  156. // How long it took in ms for data to be ready for display.
  157. highlights_data_late_by_ms: Joi.number().positive(),
  158. // Timestamp of the action perceived by the user to trigger the load
  159. // of this page.
  160. //
  161. // Not required at least for the error cases where the
  162. // observer event doesn't fire
  163. load_trigger_ts: Joi.number().positive()
  164. .notes(["server counter", "server counter alert"]),
  165. // What was the perceived trigger of the load action?
  166. //
  167. // Not required at least for the error cases where the observer event
  168. // doesn't fire
  169. load_trigger_type: Joi.valid(["first_window_opened",
  170. "menu_plus_or_keyboard", "unexpected"])
  171. .notes(["server counter", "server counter alert"]).required(),
  172. // How long it took in ms for data to be ready for display.
  173. topsites_data_late_by_ms: Joi.number().positive(),
  174. // When did the topsites element finish painting? Note that, at least for
  175. // the first tab to be loaded, and maybe some others, this will be before
  176. // topsites has yet to receive screenshots updates from the add-on code,
  177. // and is therefore just showing placeholder screenshots.
  178. topsites_first_painted_ts: Joi.number().positive()
  179. .notes(["server counter", "server counter alert"]),
  180. // Information about the quality of TopSites images and icons.
  181. topsites_icon_stats: Joi.object().keys({
  182. custom_screenshot: Joi.number(),
  183. rich_icon: Joi.number(),
  184. screenshot: Joi.number(),
  185. screenshot_with_icon: Joi.number(),
  186. tippytop: Joi.number(),
  187. no_image: Joi.number(),
  188. }),
  189. // The count of pinned Top Sites.
  190. topsites_pinned: Joi.number(),
  191. // The count of search shortcut Top Sites.
  192. topsites_search_shortcuts: Joi.number(),
  193. // When the page itself receives an event that document.visibilityState
  194. // == visible.
  195. //
  196. // Not required at least for the (error?) case where the
  197. // visibility_event doesn't fire. (It's not clear whether this
  198. // can happen in practice, but if it does, we'd like to know about it).
  199. visibility_event_rcvd_ts: Joi.number().positive()
  200. .notes(["server counter", "server counter alert"]),
  201. // The boolean to signify whether the page is preloaded or not.
  202. is_preloaded: Joi.bool().required(),
  203. }).required(),
  204. }));
  205. export const ASRouterEventPing = Joi.object().keys({
  206. client_id: Joi.string().required(),
  207. action: Joi.string().required(),
  208. impression_id: Joi.string().required(),
  209. source: Joi.string().required(),
  210. addon_version: Joi.string().required(),
  211. locale: Joi.string().required(),
  212. message_id: Joi.string().required(),
  213. event: Joi.string().required(),
  214. });
  215. export const UTSessionPing = Joi.array().items(
  216. Joi.string().required().valid("activity_stream"),
  217. Joi.string().required().valid("end"),
  218. Joi.string().required().valid("session"),
  219. Joi.string().required(),
  220. eventsTelemetryExtraKeys
  221. );
  222. export const trailheadEnrollExtraKeys = Joi.object().keys({
  223. experimentType: Joi.string().required(),
  224. branch: Joi.string().required(),
  225. }).options({allowUnknown: false});
  226. export const UTTrailheadEnrollPing = Joi.array().items(
  227. Joi.string().required().valid("activity_stream"),
  228. Joi.string().required().valid("enroll"),
  229. Joi.string().required().valid("preference_study"),
  230. Joi.string().required(),
  231. trailheadEnrollExtraKeys
  232. );
  233. export function chaiAssertions(_chai, utils) {
  234. const {Assertion} = _chai;
  235. Assertion.addMethod("validate", function(schema, schemaName) {
  236. const {error} = Joi.validate(this._obj, schema, {allowUnknown: false});
  237. this.assert(
  238. !error,
  239. `Expected to be ${schemaName ? `a valid ${schemaName}` : "valid"} but there were errors: ${error}`
  240. );
  241. });
  242. const assertions = {
  243. /**
  244. * assert.validate - Validates an item given a Joi schema
  245. *
  246. * @param {any} actual The item to validate
  247. * @param {obj} schema A Joi schema
  248. */
  249. validate(actual, schema, schemaName) {
  250. new Assertion(actual).validate(schema, schemaName);
  251. },
  252. /**
  253. * isUserEventAction - Passes if the item is a valid UserEvent action
  254. *
  255. * @param {any} actual The item to validate
  256. */
  257. isUserEventAction(actual) {
  258. new Assertion(actual).validate(UserEventAction, "UserEventAction");
  259. },
  260. };
  261. Object.assign(_chai.assert, assertions);
  262. }