import { languageKeys } from '@/languages/lib/languages-server' import { allVersionKeys } from '@/versions/lib/all-versions' import { productIds } from '@/products/lib/all-products' import { allTools } from '@/tools/lib/all-tools' import { contentTypesEnum } from '@/frame/lib/frontmatter' const versionPattern = '^\\d+(\\.\\d+)?(\\.\\d+)?$' const context = { type: 'object', additionalProperties: false, required: ['event_id', 'user', 'version', 'created', 'path'], properties: { // Required of all events event_id: { type: 'string', description: 'The unique identifier of the event.', format: 'uuid', }, user: { type: 'string', description: "The unique identifier of the current user performing the event. Please use randomly generated values or hashed values; we don't want to be able to look up in a database.", format: 'uuid', }, version: { type: 'string', description: 'The version of the event schema.', pattern: versionPattern, }, created: { type: 'string', format: 'date-time', description: 'The time we created the event; please reference UTC.', }, page_event_id: { type: 'string', description: 'The id of the corresponding `page` event.', format: 'uuid', }, // Content information referrer: { type: 'string', description: 'The browser value of `document.referrer`.', format: 'uri-reference', }, title: { type: 'string', description: 'The browser value of `document.title`.', }, href: { type: 'string', description: 'The browser value of `location.href`.', format: 'uri', }, hostname: { type: 'string', description: 'The browser value of `location.hostname.`', format: 'uri-reference', }, path: { type: 'string', description: 'The browser value of `location.pathname`.', format: 'uri-reference', }, search: { type: 'string', description: 'The browser value of `location.search`.', }, hash: { type: 'string', description: 'The browser value of `location.hash`.', }, path_language: { type: 'string', description: 'The language the user is viewing, from the URL path.', enum: languageKeys, }, path_version: { type: 'string', description: 'The GitHub version of the docs, from the URL path.', enum: allVersionKeys, }, path_product: { type: 'string', description: 'The GitHub product the docs are for, from the URL path.', enum: productIds.concat(['homepage']), }, path_article: { type: 'string', description: 'The article path without language or version, from the URL path.', }, page_document_type: { type: 'string', description: 'The generic page document type based on URL path.', enum: ['homepage', 'early-access', 'product', 'category', 'subcategory', 'article'], // get-document-type.ts }, page_type: { type: 'string', description: 'Optional page type from the content frontmatter.', enum: ['overview', 'quick_start', 'tutorial', 'how_to', 'reference', 'rai'], // frontmatter.ts }, content_type: { type: 'string', description: 'Optional content type from the content frontmatter (EDI content models).', enum: contentTypesEnum, }, status: { type: 'number', description: 'The HTTP response status code of the main page HTML.', minimum: 0, maximum: 999, }, is_logged_in: { type: 'boolean', description: 'Anonymous -- whether the user has github.com cookies set.', }, dotcom_user: { type: 'string', description: 'The cookie value of dotcom_user', }, is_staff: { type: 'boolean', description: 'The cookie value of staffonly', }, // Device information os: { type: 'string', description: 'The type of operating system the user is working with.', enum: ['windows', 'mac', 'linux', 'ios', 'android', 'cros', 'other'], default: 'other', }, os_version: { type: 'string', description: 'The version of the operating system the user is using.', }, browser: { type: 'string', description: 'The type of browser the user is browsing with.', enum: ['chrome', 'safari', 'firefox', 'edge', 'opera', 'other'], default: 'other', }, browser_version: { type: 'string', description: 'The version of the browser the user is browsing with.', }, is_headless: { type: 'boolean', }, viewport_width: { type: 'number', description: 'The viewport width, not the overall device size.', minimum: 0, }, viewport_height: { type: 'number', description: 'The viewport height, not the overall device height.', minimum: 0, }, screen_width: { type: 'number', description: 'The screen width of the device.', minimum: 0, }, screen_height: { type: 'number', description: 'The screen height of the device.', minimum: 0, }, pixel_ratio: { type: 'number', description: 'The device pixel ratio.', minimum: 0, }, ip: { type: 'string', description: 'The IP address of the user.', }, user_agent: { type: 'string', description: 'The raw user agent string from the browser.', }, // Location information timezone: { type: 'number', description: 'The timezone the user is in, as `new Date().getTimezoneOffset() / -60`.', }, user_language: { type: 'string', description: 'The browser value of `navigator.language`.', }, // Preference information os_preference: { type: 'string', enum: ['linux', 'mac', 'windows'], description: 'The os for examples selected by the user.', }, application_preference: { type: 'string', enum: Object.keys(allTools), description: 'The application selected by the user.', }, color_mode_preference: { enum: ['dark', 'light', 'auto', 'auto:dark', 'auto:light'], description: 'The color mode selected by the user.', }, code_display_preference: { enum: ['beside', 'inline'], description: 'How the user prefers to view code examples.', }, // Experiments experiment_variation: { type: 'string', description: 'The variation this user we bucketed in is in, such as control or treatment.', }, // Event Grouping. The comination of key + id should be unique event_group_key: { type: 'string', description: 'A enum indentifier (e.g. "ask-ai") used to put events into a specific group.', }, event_group_id: { type: 'string', description: 'A unique id (uuid) that can be used to identify a group of events made by a user during the same session.', }, }, } const page = { type: 'object', additionalProperties: false, required: ['type', 'context'], properties: { context, type: { type: 'string', pattern: '^page$', }, }, } const exit = { type: 'object', additionalProperties: false, required: ['type', 'context'], properties: { context, type: { type: 'string', pattern: '^exit$', }, exit_render_duration: { type: 'number', description: 'How long the server took to render the page content, in seconds.', minimum: 0.001, }, exit_first_paint: { type: 'number', minimum: 0.001, description: 'The duration until `first-contentful-paint`, in seconds. Informs CSS performance.', }, exit_dom_interactive: { type: 'number', minimum: 0.001, description: 'The duration until `PerformanceNavigationTiming.domInteractive`, in seconds. Informs JavaScript loading performance.', }, exit_dom_complete: { type: 'number', minimum: 0.001, description: 'The duration until `PerformanceNavigationTiming.domComplete`, in seconds. Informs JavaScript execution performance.', }, exit_visit_duration: { type: 'number', minimum: 0.001, description: 'The duration of exit.timestamp - page.timestamp, in seconds. Informs bounce rate.', }, exit_scroll_length: { type: 'number', minimum: 0, maximum: 1, description: 'The percentage of how far the user scrolled on the page.', }, exit_scroll_flip: { type: 'number', minimum: 0, description: 'The number of times the scroll direction changes.', }, }, } const keyboard = { type: 'object', additionalProperties: false, required: ['pressed_key', 'pressed_on'], properties: { context, type: { type: 'string', pattern: '^keyboard$', }, pressed_key: { type: 'string', description: 'The key the user pressed.', }, pressed_on: { type: 'string', description: 'The element/identifier the user pressed the key on.', }, }, } const link = { type: 'object', additionalProperties: false, required: ['type', 'context', 'link_url'], properties: { context, type: { type: 'string', pattern: '^link$', }, link_url: { type: 'string', format: 'uri', description: 'The href of the anchor tag the user clicked, or the page or object they directed their browser to.', }, link_samesite: { type: 'boolean', description: 'If the link stays on docs.github.com.', }, link_samepage: { type: 'boolean', description: 'If the link stays on the same page (hash link).', }, link_container: { type: 'string', enum: [ 'header', 'nav', 'breadcrumbs', 'title', 'lead', 'notifications', 'article', 'alert', 'toc', 'footer', 'static', ], description: 'The part of the page where the user clicked the link.', }, }, } const hover = { type: 'object', additionalProperties: false, required: ['type', 'context', 'hover_url'], properties: { context, type: { type: 'string', pattern: '^hover$', }, hover_url: { type: 'string', format: 'uri', description: 'The href of the anchor tag the user hovered, or the page or object they directed their browser to.', }, hover_samesite: { type: 'boolean', description: 'If the hover link stays on docs.github.com.', }, }, } const search = { type: 'object', additionalProperties: false, required: ['type', 'context', 'search_query'], properties: { context, type: { type: 'string', pattern: '^search$', }, search_query: { type: 'string', description: 'The actual text content of the query string the user sent to the service.', }, search_context: { type: 'string', description: 'Any additional search context, such as component searched.', }, search_client: { type: 'string', description: 'The client name identifier when the request is not from docs.github.com.', }, }, } const searchResult = { type: 'object', additionalProperties: false, required: [ 'type', 'context', 'search_result_query', 'search_result_index', 'search_result_total', 'search_result_rank', 'search_result_url', ], properties: { context, type: { type: 'string', pattern: '^searchResult$', }, search_result_query: { type: 'string', description: 'The query the user searched for.', }, search_result_index: { type: 'number', description: 'The order position of the user selected search result.', }, search_result_total: { type: 'number', description: 'The total number of search results we returned for the query.', }, search_result_rank: { type: 'number', description: 'The rank score of the order position of the search result, example: `(total - index) / total`.', }, search_result_url: { type: 'string', description: 'The destination url of the search result the user selected.', }, }, } const aiSearchResult = { type: 'object', additionalProperties: false, required: [ 'type', 'context', 'ai_search_result_links_json', 'ai_search_result_provided_answer', 'ai_search_result_response_status', ], properties: { context, type: { type: 'string', pattern: '^aiSearchResult$', }, ai_search_result_links_json: { type: 'string', description: 'Dynamic JSON string of an array of "link" objects in the form: [{ "type": "reference" | "inline", "url": "https://..", "product": "issues" | "pages" | ... }, ...]', }, ai_search_result_provided_answer: { type: 'boolean', description: 'Whether the GPT was able to answer the query.', }, ai_search_result_response_status: { type: 'number', description: 'The status code of the GPT response.', }, ai_search_result_connected_event_id: { type: 'string', description: 'The id of the corresponding CSE copilot conversation event.', }, }, } const survey = { type: 'object', additionalProperties: false, required: ['type', 'context', 'survey_vote'], properties: { context, type: { type: 'string', pattern: '^survey$', }, survey_vote: { type: 'boolean', description: 'Whether the user found the page helpful.', }, survey_comment: { type: 'string', description: 'Any textual comments the user wanted to provide.', }, survey_email: { type: 'string', format: 'email', description: "The user's email address, if the user provided and consented.", }, survey_rating: { type: 'number', description: 'The computed rating of the quality of the survey comment. Used for spam filtering and quality control.', }, survey_comment_language: { type: 'string', description: 'The guessed language of the survey comment. The guessed language is very inaccurate when the string contains fewer than 3 or 4 words.', }, survey_connected_event_id: { type: 'string', description: 'The id of the corresponding CSE copilot conversation event.', }, }, } const experiment = { type: 'object', additionalProperties: false, required: ['type', 'context', 'experiment_name', 'experiment_variation'], properties: { context, type: { type: 'string', pattern: '^experiment$', }, experiment_name: { type: 'string', description: 'The test that this event is part of.', }, experiment_variation: { type: 'string', description: 'The variation this user we bucketed in is in, such as control or treatment.', }, experiment_success: { type: 'boolean', default: true, description: 'Whether or not the user successfully performed the test goal.', }, }, } const clipboard = { type: 'object', additionalProperties: false, required: ['type', 'context', 'clipboard_operation'], properties: { context, type: { type: 'string', pattern: '^clipboard$', }, clipboard_operation: { type: 'string', description: 'Which clipboard operation the user is performing.', enum: ['copy', 'paste', 'cut'], }, clipboard_target: { type: 'string', description: 'How the user got the contents into their clipboard.', }, }, } const print = { type: 'object', additionalProperties: false, required: ['type', 'context'], properties: { context, type: { type: 'string', pattern: '^print$', }, }, } const preference = { type: 'object', additionalProperties: false, required: ['type', 'context', 'preference_name', 'preference_value'], properties: { context, type: { type: 'string', pattern: '^preference$', }, preference_name: { type: 'string', enum: ['application', 'color_mode', 'os', 'code_display'], description: 'The preference name, such as os, application, or color_mode', }, preference_value: { type: 'string', enum: [ // application ...Object.keys(allTools), // color_mode 'dark', 'light', 'auto', 'auto:dark', 'auto:light', // os 'linux', 'mac', 'windows', // code_display 'beside', 'inline', ], description: 'The application, color_mode, os, or code_display selected by the user.', }, }, } const validation = { type: 'object', additionalProperties: false, properties: { event_id: { type: 'string', format: 'uuid' }, version: { type: 'string', pattern: versionPattern }, created: { type: 'string', format: 'date-time' }, raw: { type: 'string' }, // https://ajv.js.org/api.html#error-objects keyword: { type: 'string' }, instance_path: { type: 'string' }, schema_path: { type: 'string' }, params: { type: 'string' }, property_name: { type: 'string' }, message: { type: 'string' }, schema: { type: 'string' }, parent_schema: { type: 'string' }, data: { type: 'string' }, }, } // We are not using `oneOf` to keep the list of errors short. export const schemas = { page, exit, keyboard, link, hover, search, searchResult, aiSearchResult, survey, experiment, clipboard, print, preference, validation, } export const hydroNames = { page: 'docs.v0.PageEvent', exit: 'docs.v0.ExitEvent', keyboard: 'docs.v0.KeyboardEvent', link: 'docs.v0.LinkEvent', hover: 'docs.v0.HoverEvent', search: 'docs.v0.SearchEvent', searchResult: 'docs.v0.SearchResultEvent', aiSearchResult: 'docs.v0.AISearchResultEvent', survey: 'docs.v0.SurveyEvent', experiment: 'docs.v0.ExperimentEvent', clipboard: 'docs.v0.ClipboardEvent', print: 'docs.v0.PrintEvent', preference: 'docs.v0.PreferenceEvent', validation: 'docs.v0.ValidationEvent', } as Record const schemasKeys = Object.keys(schemas) const hydroNamesKeys = Object.keys(hydroNames) if ( schemasKeys.length !== hydroNamesKeys.length || !schemasKeys.every((k) => hydroNamesKeys.includes(k)) ) { throw new Error("The keys in 'schemas' doesn't match with the keys in 'hydroNames'") }