telemetry.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.sendMetric = exports.telemetryAction = exports.THANK_YOU = void 0;
  4. const tslib_1 = require("tslib");
  5. const commander_1 = require("commander");
  6. const debug_1 = tslib_1.__importDefault(require("debug"));
  7. const colors_1 = tslib_1.__importDefault(require("./colors"));
  8. const ipc_1 = require("./ipc");
  9. const log_1 = require("./log");
  10. const sysconfig_1 = require("./sysconfig");
  11. const subprocess_1 = require("./util/subprocess");
  12. const term_1 = require("./util/term");
  13. const debug = (0, debug_1.default)('capacitor:telemetry');
  14. exports.THANK_YOU = `\nThank you for helping to make Capacitor better! 💖` +
  15. `\nInformation about the data we collect is available on our website: ${colors_1.default.strong('https://capacitorjs.com/telemetry')}\n`;
  16. function telemetryAction(config, action) {
  17. return async (...actionArgs) => {
  18. const start = new Date();
  19. // This is how commanderjs works--the command object is either the last
  20. // element or second to last if there are additional options (via `.allowUnknownOption()`)
  21. const lastArg = actionArgs[actionArgs.length - 1];
  22. const cmd = lastArg instanceof commander_1.Command ? lastArg : actionArgs[actionArgs.length - 2];
  23. const command = getFullCommandName(cmd);
  24. let error;
  25. try {
  26. await action(...actionArgs);
  27. }
  28. catch (e) {
  29. error = e;
  30. }
  31. const end = new Date();
  32. const duration = end.getTime() - start.getTime();
  33. const packages = Object.entries({
  34. ...config.app.package.devDependencies,
  35. ...config.app.package.dependencies,
  36. });
  37. // Only collect packages in the capacitor org:
  38. // https://www.npmjs.com/org/capacitor
  39. const capacitorPackages = packages.filter(([k]) => k.startsWith('@capacitor/'));
  40. const versions = capacitorPackages.map(([k, v]) => [
  41. `${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`,
  42. v,
  43. ]);
  44. const data = {
  45. app_id: await getAppIdentifier(config),
  46. command,
  47. arguments: cmd.args.join(' '),
  48. options: JSON.stringify(cmd.opts()),
  49. duration,
  50. error: error ? (error.message ? error.message : String(error)) : null,
  51. node_version: process.version,
  52. os: config.cli.os,
  53. ...Object.fromEntries(versions),
  54. };
  55. if ((0, term_1.isInteractive)()) {
  56. let sysconfig = await (0, sysconfig_1.readConfig)();
  57. if (!error && typeof sysconfig.telemetry === 'undefined') {
  58. const confirm = await promptForTelemetry();
  59. sysconfig = { ...sysconfig, telemetry: confirm };
  60. await (0, sysconfig_1.writeConfig)(sysconfig);
  61. }
  62. await sendMetric(sysconfig, 'capacitor_cli_command', data);
  63. }
  64. if (error) {
  65. throw error;
  66. }
  67. };
  68. }
  69. exports.telemetryAction = telemetryAction;
  70. /**
  71. * If telemetry is enabled, send a metric via IPC to a forked process for uploading.
  72. */
  73. async function sendMetric(sysconfig, name, data) {
  74. if (sysconfig.telemetry && (0, term_1.isInteractive)()) {
  75. const message = {
  76. name,
  77. timestamp: new Date().toISOString(),
  78. session_id: sysconfig.machine,
  79. source: 'capacitor_cli',
  80. value: data,
  81. };
  82. await (0, ipc_1.send)({ type: 'telemetry', data: message });
  83. }
  84. else {
  85. debug('Telemetry is off (user choice, non-interactive terminal, or CI)--not sending metric');
  86. }
  87. }
  88. exports.sendMetric = sendMetric;
  89. async function promptForTelemetry() {
  90. const { confirm } = await (0, log_1.logPrompt)(`${colors_1.default.strong('Would you like to help improve Capacitor by sharing anonymous usage data? 💖')}\n` +
  91. `Read more about what is being collected and why here: ${colors_1.default.strong('https://capacitorjs.com/telemetry')}. You can change your mind at any time by using the ${colors_1.default.input('npx cap telemetry')} command.`, {
  92. type: 'confirm',
  93. name: 'confirm',
  94. message: 'Share anonymous usage data?',
  95. initial: true,
  96. });
  97. if (confirm) {
  98. log_1.output.write(exports.THANK_YOU);
  99. }
  100. return confirm;
  101. }
  102. /**
  103. * Get a unique anonymous identifier for this app.
  104. */
  105. async function getAppIdentifier(config) {
  106. const { createHash } = await Promise.resolve().then(() => tslib_1.__importStar(require('crypto')));
  107. // get the first commit hash, which should be universally unique
  108. const output = await (0, subprocess_1.getCommandOutput)('git', ['rev-list', '--max-parents=0', 'HEAD'], { cwd: config.app.rootDir });
  109. const firstLine = output === null || output === void 0 ? void 0 : output.split('\n')[0];
  110. if (!firstLine) {
  111. debug('Could not obtain unique app identifier');
  112. return null;
  113. }
  114. // use sha1 to create a one-way hash to anonymize
  115. const id = createHash('sha1').update(firstLine).digest('hex');
  116. return id;
  117. }
  118. /**
  119. * Walk through the command's parent tree and construct a space-separated name.
  120. *
  121. * Probably overkill because we don't have nested commands, but whatever.
  122. */
  123. function getFullCommandName(cmd) {
  124. const names = [];
  125. while (cmd.parent !== null) {
  126. names.push(cmd.name());
  127. cmd = cmd.parent;
  128. }
  129. return names.reverse().join(' ');
  130. }