import {fetch} from './http.js' import {outputDebug, outputContent, outputToken} from './output.js' import {JsonMap} from '../../private/common/json.js' import {DeepRequired} from '../common/ts/deep-required.js' export {DeepRequired} const url = 'https://monorail-edge.shopifysvc.com/v1/produce' type Optional = T | null // This is the topic name of the main event we log to Monorail, the command tracker export const MONORAIL_COMMAND_TOPIC = 'app_cli3_command/1.30' export interface Schemas { [MONORAIL_COMMAND_TOPIC]: { sensitive: { args: string error_message?: Optional app_name?: Optional metadata?: Optional store_fqdn?: Optional cmd_all_environment_flags?: Optional // Dev related commands cmd_dev_tunnel_custom?: Optional // Environment env_plugin_installed_all?: Optional env_shopify_variables?: Optional } public: { business_platform_id?: Optional partner_id?: Optional store_id?: Optional command: string project_type?: Optional time_start: number time_end: number total_time: number success: boolean api_key?: Optional cli_version: string uname: string ruby_version: string node_version: string is_employee: boolean store_fqdn_hash?: Optional store_fqdn_validated?: Optional store_domain?: Optional user_id: string // Any and all commands cmd_all_alias_used?: Optional cmd_all_launcher?: Optional cmd_all_path_override?: Optional cmd_all_path_override_hash?: Optional cmd_all_plugin?: Optional cmd_all_topic?: Optional cmd_all_verbose?: Optional cmd_all_exit?: Optional cmd_all_force?: Optional cmd_all_last_graphql_request_id?: Optional cmd_all_timing_network_ms?: Optional cmd_all_timing_prompts_ms?: Optional cmd_all_timing_active_ms?: Optional // Auto-upgrade env_auto_upgrade_enabled?: Optional env_auto_upgrade_accepted?: Optional env_auto_upgrade_skipped_reason?: Optional env_auto_upgrade_success?: Optional // Any extension related command cmd_extensions_binary_from_source?: Optional // Scaffolding related commands cmd_scaffold_required_auth?: Optional cmd_scaffold_template_custom?: Optional cmd_scaffold_template_flavor?: Optional cmd_scaffold_type?: Optional cmd_scaffold_type_category?: Optional cmd_scaffold_type_gated?: Optional cmd_scaffold_type_owner?: Optional cmd_scaffold_used_prompts_for_type?: Optional // Used in several but not all commands cmd_app_dependency_installation_skipped?: Optional cmd_app_reset_used?: Optional cmd_app_linked_config_used?: Optional cmd_app_linked_config_name?: Optional cmd_app_linked_config_git_tracked?: Optional cmd_app_all_configs_any?: Optional cmd_app_all_configs_clients?: Optional cmd_app_linked_config_source?: Optional cmd_app_linked_config_uses_cli_managed_urls?: Optional cmd_app_warning_api_key_deprecation_displayed?: Optional cmd_app_deployment_mode?: Optional cmd_app_validate_json?: Optional cmd_app_validate_valid?: Optional cmd_app_validate_issue_count?: Optional cmd_app_validate_file_count?: Optional // app security commands num_security_findings?: Optional num_security_findings_resolved?: Optional // Dev related commands cmd_dev_tunnel_type?: Optional cmd_dev_tunnel_custom_hash?: Optional cmd_dev_urls_updated?: Optional cmd_dev_preview_url_opened?: Optional cmd_dev_graphiql_opened?: Optional cmd_dev_dev_preview_toggle_used?: Optional // Create-app related commands cmd_create_app_template?: Optional cmd_create_app_template_url?: Optional // Deploy related commands cmd_deploy_flag_message_used?: Optional cmd_deploy_flag_version_used?: Optional cmd_deploy_flag_source_url_used?: Optional cmd_deploy_confirm_new_registrations?: Optional cmd_deploy_confirm_updated_registrations?: Optional cmd_deploy_confirm_removed_registrations?: Optional cmd_deploy_confirm_cancelled?: Optional cmd_deploy_confirm_time_to_complete_ms?: Optional cmd_deploy_prompt_upgrade_to_unified_displayed?: Optional cmd_deploy_prompt_upgrade_to_unified_response?: Optional cmd_deploy_confirm_include_config_used?: Optional cmd_deploy_include_config_used?: Optional cmd_deploy_config_modules_breakdown?: Optional cmd_deploy_config_modules_updated?: Optional cmd_deploy_config_modules_added?: Optional cmd_deploy_config_modules_deleted?: Optional // Release related commands cmd_release_confirm_cancelled?: Optional // App setup app_extensions_any?: Optional app_extensions_breakdown?: Optional app_extensions_count?: Optional app_extensions_custom_layout?: Optional app_extensions_function_any?: Optional app_extensions_function_count?: Optional app_extensions_function_custom_layout?: Optional app_extensions_theme_any?: Optional app_extensions_theme_count?: Optional app_extensions_theme_custom_layout?: Optional app_extensions_ui_any?: Optional app_extensions_ui_count?: Optional app_extensions_ui_custom_layout?: Optional app_name_hash?: Optional app_path_hash?: Optional app_scopes?: Optional app_web_backend_any?: Optional app_web_backend_count?: Optional app_web_custom_layout?: Optional app_web_framework?: Optional app_web_frontend_any?: Optional app_web_frontend_count?: Optional // Theme related commands cmd_theme_timings?: Optional cmd_theme_errors?: Optional cmd_theme_retries?: Optional cmd_theme_events?: Optional // Environment env_ci?: Optional env_ci_platform?: Optional env_device_id?: Optional env_package_manager?: Optional env_install_package_manager?: Optional env_package_manager_workspaces?: Optional env_plugin_installed_any_custom?: Optional env_plugin_installed_shopify?: Optional env_shell?: Optional env_web_ide?: Optional env_cloud?: Optional env_is_global?: Optional env_auth_method?: Optional } } [schemaId: string]: {sensitive: JsonMap; public: JsonMap} } // In reality, we're normally most interested in just this from Schemas, so export it for ease of use. // The monorail schema itself has lots of optional values as it must be backwards-compatible. For our schema we want mandatory values instead. export type MonorailEventPublic = DeepRequired export type MonorailEventSensitive = Schemas[typeof MONORAIL_COMMAND_TOPIC]['sensitive'] type MonorailResult = {type: 'ok'} | {type: 'error'; message: string} const publishedCommandNames = new Set() /** * Publishes an event to Monorail. * * @param schemaId - The schema ID of the event to publish. * @param publicData - The public data to publish. * @param sensitiveData - The sensitive data to publish. * @returns A result indicating whether the event was successfully published. */ export async function publishMonorailEvent( schemaId: TSchemaId, publicData: TPayload['public'], sensitiveData: TPayload['sensitive'], ): Promise { // If a command has already been logged, never re-log it. This is to prevent duplication caused by unexpected errors. const commandName = publicData.command if (commandName && typeof commandName === 'string') { if (publishedCommandNames.has(commandName)) { return {type: 'ok'} } publishedCommandNames.add(commandName) } try { const currentTime = new Date().getTime() const payload = {...publicData, ...sensitiveData} const body = JSON.stringify({schema_id: schemaId, payload}) const headers = buildHeaders(currentTime) const response = await fetch(url, {method: 'POST', body, headers}, 'non-blocking') if (response.status === 200) { outputDebug(outputContent`Analytics event sent: ${outputToken.json(sanitizePayload(payload))}`) return {type: 'ok'} } else { outputDebug(`Failed to report usage analytics: ${response.statusText}`) return {type: 'error', message: response.statusText} } // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { let message = 'Failed to report usage analytics' if (error instanceof Error) { message = message.concat(`: ${error.message}`) } outputDebug(message) return {type: 'error', message} } } /** * Sanitizies the api_key from the payload and returns a new hash. * * @param payload - The public and sensitive data. * @returns A copy of the payload with the api_key sanitized. */ function sanitizePayload(payload: T): T { const result = {...payload} if ('api_key' in result) { result.api_key = '****' } return result } const buildHeaders = (currentTime: number) => { return { 'Content-Type': 'application/json; charset=utf-8', 'X-Monorail-Edge-Event-Created-At-Ms': currentTime.toString(), 'X-Monorail-Edge-Event-Sent-At-Ms': currentTime.toString(), } }