nuxt-telemetry.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. #!/usr/bin/env node
  2. 'use strict';
  3. const destr = require('destr');
  4. const nanoid = require('nanoid');
  5. const rc9 = require('rc9');
  6. const fetch = require('node-fetch');
  7. const path = require('path');
  8. const fs = require('fs');
  9. const createRequire = require('create-require');
  10. const os = require('os');
  11. const gitUrlParse = require('git-url-parse');
  12. const parseGitConfig = require('parse-git-config');
  13. const isDocker = require('is-docker');
  14. const ci = require('ci-info');
  15. const fs$1 = require('fs-extra');
  16. const crypto = require('crypto');
  17. const consola = require('consola');
  18. const c = require('chalk');
  19. const inquirer = require('inquirer');
  20. const stdEnv = require('std-env');
  21. function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
  22. const destr__default = /*#__PURE__*/_interopDefaultLegacy(destr);
  23. const fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
  24. const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
  25. const createRequire__default = /*#__PURE__*/_interopDefaultLegacy(createRequire);
  26. const os__default = /*#__PURE__*/_interopDefaultLegacy(os);
  27. const gitUrlParse__default = /*#__PURE__*/_interopDefaultLegacy(gitUrlParse);
  28. const parseGitConfig__default = /*#__PURE__*/_interopDefaultLegacy(parseGitConfig);
  29. const isDocker__default = /*#__PURE__*/_interopDefaultLegacy(isDocker);
  30. const ci__default = /*#__PURE__*/_interopDefaultLegacy(ci);
  31. const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs$1);
  32. const consola__default = /*#__PURE__*/_interopDefaultLegacy(consola);
  33. const c__default = /*#__PURE__*/_interopDefaultLegacy(c);
  34. const inquirer__default = /*#__PURE__*/_interopDefaultLegacy(inquirer);
  35. const stdEnv__default = /*#__PURE__*/_interopDefaultLegacy(stdEnv);
  36. var name = "@nuxt/telemetry";
  37. var version = "1.3.6";
  38. function updateUserNuxtRc(key, val) {
  39. rc9.updateUser({[key]: val}, ".nuxtrc");
  40. }
  41. const consentVersion = 1;
  42. async function postEvent(endpoint, body) {
  43. const res = await fetch__default['default'](endpoint, {
  44. method: "POST",
  45. body: JSON.stringify(body),
  46. headers: {
  47. "content-type": "application/json",
  48. "user-agent": "Nuxt Telemetry " + version
  49. },
  50. timeout: 4e3
  51. });
  52. if (!res.ok) {
  53. throw new Error(res.statusText);
  54. }
  55. }
  56. const build = function({nuxt}, payload) {
  57. const duration = {build: payload.duration.build};
  58. let isSuccess = true;
  59. for (const [name, stat] of Object.entries(payload.stats)) {
  60. duration[name] = stat.duration;
  61. if (!stat.success) {
  62. isSuccess = false;
  63. }
  64. }
  65. return {
  66. name: "build",
  67. isSuccess,
  68. isDev: nuxt.options.dev || false,
  69. duration
  70. };
  71. };
  72. const command = function({nuxt}) {
  73. let command2 = "unknown";
  74. const flagMap = {
  75. dev: "dev",
  76. _generate: "generate",
  77. _export: "export",
  78. _build: "build",
  79. _serve: "serve",
  80. _start: "start"
  81. };
  82. for (const flag in flagMap) {
  83. if (nuxt.options[flag]) {
  84. command2 = flagMap[flag];
  85. break;
  86. }
  87. }
  88. return {
  89. name: "command",
  90. command: command2
  91. };
  92. };
  93. const generate = function generate2({nuxt}, payload) {
  94. return {
  95. name: "generate",
  96. isExport: !!nuxt.options._export,
  97. routesCount: payload.routesCount,
  98. duration: {
  99. generate: payload.duration.generate
  100. }
  101. };
  102. };
  103. const dependency = function({nuxt: {options}}) {
  104. const events = [];
  105. const projectDeps = getDependencies(options.rootDir);
  106. const modules = normalizeModules(options.modules);
  107. const buildModules = normalizeModules(options.buildModules);
  108. const relatedDeps = [...modules, ...buildModules];
  109. for (const dep of projectDeps) {
  110. if (!relatedDeps.includes(dep.name)) {
  111. continue;
  112. }
  113. events.push({
  114. name: "dependency",
  115. packageName: dep.name,
  116. version: dep.version,
  117. isDevDependency: dep.dev,
  118. isModule: modules.includes(dep.name),
  119. isBuildModule: buildModules.includes(dep.name)
  120. });
  121. }
  122. return events;
  123. };
  124. function normalizeModules(modules) {
  125. return modules.map((m) => {
  126. if (typeof m === "string") {
  127. return m;
  128. }
  129. if (Array.isArray(m) && typeof m[0] === "string") {
  130. return m[0];
  131. }
  132. return null;
  133. }).filter(Boolean);
  134. }
  135. function getDependencies(rootDir) {
  136. const pkgPath = path.join(rootDir, "package.json");
  137. if (!fs.existsSync(pkgPath)) {
  138. return [];
  139. }
  140. const _require = createRequire__default['default'](rootDir);
  141. const pkg = _require(pkgPath);
  142. const mapDeps = (depsObj, dev = false) => {
  143. const _deps = [];
  144. for (const name in depsObj) {
  145. try {
  146. const pkg2 = _require(path.join(name, "package.json"));
  147. _deps.push({name, version: pkg2.version, dev});
  148. } catch (_e) {
  149. _deps.push({name, version: depsObj[name], dev});
  150. }
  151. }
  152. return _deps;
  153. };
  154. const deps = [];
  155. if (pkg.dependencies) {
  156. deps.push(...mapDeps(pkg.dependencies));
  157. }
  158. if (pkg.devDependencies) {
  159. deps.push(...mapDeps(pkg.dependencies, true));
  160. }
  161. return deps;
  162. }
  163. const project = function(context) {
  164. const {options} = context.nuxt;
  165. return {
  166. name: "project",
  167. type: context.git && context.git.url ? "git" : "local",
  168. isSSR: options.mode === "universal" || options.ssr === true,
  169. target: options._generate ? "static" : "server",
  170. packageManager: context.packageManager
  171. };
  172. };
  173. const session = function({seed}) {
  174. return {
  175. name: "session",
  176. id: seed
  177. };
  178. };
  179. const events = /*#__PURE__*/Object.freeze({
  180. __proto__: null,
  181. build: build,
  182. command: command,
  183. generate: generate,
  184. dependency: dependency,
  185. getDependencies: getDependencies,
  186. project: project,
  187. session: session
  188. });
  189. const FILE2PM = {
  190. "yarn.lock": "yarn",
  191. "package-lock.json": "npm",
  192. "shrinkwrap.json": "npm"
  193. };
  194. async function detectPackageManager(rootDir) {
  195. for (const file in FILE2PM) {
  196. if (await fs__default['default'].pathExists(path__default['default'].resolve(rootDir, file))) {
  197. return FILE2PM[file];
  198. }
  199. }
  200. return "unknown";
  201. }
  202. function hash(str) {
  203. return crypto.createHash("sha256").update(str).digest("hex").substr(0, 16);
  204. }
  205. async function createContext(nuxt, options) {
  206. const rootDir = nuxt.options.rootDir || process.cwd();
  207. const git = await getGit(rootDir);
  208. const packageManager = await detectPackageManager(rootDir);
  209. const {seed} = options;
  210. const projectHash = await getProjectHash(rootDir, git, seed);
  211. const projectSession = getProjectSession(projectHash, seed);
  212. const nuxtVersion = (nuxt.constructor.version || "").replace("v", "");
  213. const nodeVersion = process.version.replace("v", "");
  214. const isEdge = nuxtVersion.includes("-");
  215. return {
  216. nuxt,
  217. seed,
  218. git,
  219. projectHash,
  220. projectSession,
  221. nuxtVersion,
  222. isEdge,
  223. cli: getCLI(),
  224. nodeVersion,
  225. os: os__default['default'].type().toLocaleLowerCase(),
  226. environment: getEnv(),
  227. packageManager,
  228. concent: options.consent
  229. };
  230. }
  231. function getEnv() {
  232. if (process.env.CODESANDBOX_SSE) {
  233. return "CSB";
  234. }
  235. if (ci__default['default'].isCI) {
  236. return ci__default['default'].name;
  237. }
  238. if (isDocker__default['default']()) {
  239. return "Docker";
  240. }
  241. return "unknown";
  242. }
  243. function getCLI() {
  244. let entry;
  245. if (typeof require !== "undefined" && require.main && require.main.filename) {
  246. entry = require.main.filename;
  247. } else {
  248. entry = process.argv[1];
  249. }
  250. const knownCLIs = {
  251. "nuxt-ts.js": "nuxt-ts",
  252. "nuxt-start.js": "nuxt-start",
  253. "nuxt.js": "nuxt"
  254. };
  255. for (const key in knownCLIs) {
  256. if (entry.includes(key)) {
  257. const edge = entry.includes("-edge") ? "-edge" : "";
  258. return knownCLIs[key] + edge;
  259. }
  260. }
  261. return "programmatic";
  262. }
  263. function getProjectSession(projectHash, sessionId) {
  264. return hash(`${projectHash}#${sessionId}`);
  265. }
  266. function getProjectHash(rootDir, git, seed) {
  267. let id;
  268. if (git && git.url) {
  269. id = `${git.source}#${git.owner}#${git.name}`;
  270. } else {
  271. id = `${rootDir}#${seed}`;
  272. }
  273. return hash(id);
  274. }
  275. async function getGitRemote(rootDir) {
  276. try {
  277. const parsed = await parseGitConfig__default['default']({cwd: rootDir});
  278. if (parsed) {
  279. const gitRemote = parsed['remote "origin"'].url;
  280. return gitRemote;
  281. }
  282. return null;
  283. } catch (err) {
  284. return null;
  285. }
  286. }
  287. async function getGit(rootDir) {
  288. const gitRemote = await getGitRemote(rootDir);
  289. if (!gitRemote) {
  290. return;
  291. }
  292. const meta = gitUrlParse__default['default'](gitRemote);
  293. const url = meta.toString("https");
  294. return {
  295. url,
  296. gitRemote,
  297. source: meta.source,
  298. owner: meta.owner,
  299. name: meta.name
  300. };
  301. }
  302. const log = consola__default['default'].withScope("@nuxt/telemetry");
  303. class Telemetry {
  304. constructor(nuxt, options) {
  305. this.events = [];
  306. this.nuxt = nuxt;
  307. this.options = options;
  308. }
  309. getContext() {
  310. if (!this._contextPromise) {
  311. this._contextPromise = createContext(this.nuxt, this.options);
  312. }
  313. return this._contextPromise;
  314. }
  315. createEvent(name, payload) {
  316. const eventFactory = events[name];
  317. if (typeof eventFactory !== "function") {
  318. log.warn("Unknown event:", name);
  319. return;
  320. }
  321. const eventPromise = this._invokeEvent(name, eventFactory, payload);
  322. this.events.push(eventPromise);
  323. }
  324. async _invokeEvent(name, eventFactory, payload) {
  325. try {
  326. const context = await this.getContext();
  327. const event = await eventFactory(context, payload);
  328. event.name = name;
  329. return event;
  330. } catch (err) {
  331. log.error("Error while running event:", err);
  332. }
  333. }
  334. async getPublicContext() {
  335. const context = await this.getContext();
  336. const eventContext = {};
  337. for (const key of [
  338. "nuxtVersion",
  339. "isEdge",
  340. "nodeVersion",
  341. "cli",
  342. "os",
  343. "environment",
  344. "projectHash",
  345. "projectSession"
  346. ]) {
  347. eventContext[key] = context[key];
  348. }
  349. return eventContext;
  350. }
  351. async sendEvents() {
  352. const events2 = [].concat(...(await Promise.all(this.events)).filter(Boolean));
  353. this.events = [];
  354. const context = await this.getPublicContext();
  355. const body = {
  356. timestamp: Date.now(),
  357. context,
  358. events: events2
  359. };
  360. if (this.options.endpoint) {
  361. const start = Date.now();
  362. try {
  363. log.info("Sending events:", JSON.stringify(body, null, 2));
  364. await postEvent(this.options.endpoint, body);
  365. log.success(`Events sent to \`${this.options.endpoint}\` (${Date.now() - start} ms)`);
  366. } catch (err) {
  367. log.error(`Error sending sent to \`${this.options.endpoint}\` (${Date.now() - start} ms)
  368. `, err);
  369. }
  370. }
  371. }
  372. }
  373. function getStats(stats) {
  374. const duration = stats.endTime - stats.startTime;
  375. return {
  376. duration,
  377. success: stats.compilation.errors.length === 0,
  378. size: 0,
  379. fullHash: stats.compilation.fullHash
  380. };
  381. }
  382. async function ensureUserconsent(options) {
  383. if (options.consent >= consentVersion) {
  384. return true;
  385. }
  386. if (stdEnv__default['default'].minimal || process.env.CODESANDBOX_SSE || process.env.NEXT_TELEMETRY_DISABLED || isDocker__default['default']()) {
  387. return false;
  388. }
  389. process.stdout.write("\n");
  390. consola__default['default'].info(`${c__default['default'].green("NuxtJS")} collects completely anonymous data about usage.
  391. This will help us improve Nuxt developer experience over time.
  392. Read more on ${c__default['default'].cyan.underline("https://git.io/nuxt-telemetry")}
  393. `);
  394. const {accept} = await inquirer__default['default'].prompt({
  395. type: "confirm",
  396. name: "accept",
  397. message: "Are you interested in participating?"
  398. });
  399. process.stdout.write("\n");
  400. if (accept) {
  401. updateUserNuxtRc("telemetry.consent", consentVersion);
  402. updateUserNuxtRc("telemetry.enabled", true);
  403. return true;
  404. }
  405. updateUserNuxtRc("telemetry.enabled", false);
  406. return false;
  407. }
  408. async function _telemetryModule(nuxt) {
  409. const toptions = {
  410. endpoint: destr__default['default'](process.env.NUXT_TELEMETRY_ENDPOINT) || "https://telemetry.nuxtjs.com",
  411. debug: destr__default['default'](process.env.NUXT_TELEMETRY_DEBUG),
  412. ...nuxt.options.telemetry
  413. };
  414. if (!toptions.debug) {
  415. log.level = -Infinity;
  416. }
  417. if (nuxt.options.telemetry !== true) {
  418. if (toptions.enabled === false || nuxt.options.telemetry === false || !await ensureUserconsent(toptions)) {
  419. log.info("Telemetry disabled");
  420. return;
  421. }
  422. }
  423. log.info("Telemetry enabled");
  424. if (!toptions.seed) {
  425. toptions.seed = hash(nanoid.nanoid());
  426. updateUserNuxtRc("telemetry.seed", toptions.seed);
  427. log.info("Seed generated:", toptions.seed);
  428. }
  429. const t = new Telemetry(nuxt, toptions);
  430. if (nuxt.options._start) {
  431. nuxt.hook("listen", () => {
  432. t.createEvent("project");
  433. t.createEvent("session");
  434. t.createEvent("command");
  435. t.sendEvents();
  436. });
  437. }
  438. nuxt.hook("build:before", () => {
  439. t.createEvent("project");
  440. t.createEvent("session");
  441. t.createEvent("command");
  442. t.createEvent("dependency");
  443. });
  444. profile(nuxt, t);
  445. }
  446. const telemetryModule = async function() {
  447. try {
  448. await _telemetryModule(this.nuxt);
  449. } catch (err) {
  450. log.error(err);
  451. }
  452. };
  453. function profile(nuxt, t) {
  454. const startT = {};
  455. const duration = {};
  456. const stats = {};
  457. let routesCount = 0;
  458. const timeStart = (name2) => {
  459. startT[name2] = Date.now();
  460. };
  461. const timeEnd = (name2) => {
  462. duration[name2] = Date.now() - startT[name2];
  463. };
  464. nuxt.hook("build:before", () => {
  465. timeStart("build");
  466. });
  467. nuxt.hook("build:done", () => {
  468. timeEnd("build");
  469. });
  470. nuxt.hook("build:compiled", ({name: name2, stats: _stats}) => {
  471. stats[name2] = getStats(_stats);
  472. });
  473. nuxt.hook("generate:extendRoutes", () => timeStart("generate"));
  474. nuxt.hook("generate:routeCreated", () => {
  475. routesCount++;
  476. });
  477. nuxt.hook("generate:done", () => {
  478. timeEnd("generate");
  479. t.createEvent("generate", {duration, stats, routesCount});
  480. t.sendEvents();
  481. });
  482. nuxt.hook("build:done", () => {
  483. t.createEvent("build", {duration, stats});
  484. t.sendEvents();
  485. });
  486. }
  487. telemetryModule.meta = {name, version};
  488. module.exports = telemetryModule;