diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index 08efc210507e..1657cf339f3e 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -24,15 +24,21 @@ "@babel/core": "7.20.12", "@babel/node": "7.20.7", "@babel/runtime-corejs3": "7.20.13", - "@redwoodjs/internal": "3.2.0", - "@redwoodjs/telemetry": "3.2.0", + "@opentelemetry/api": "1.4.0", + "@opentelemetry/exporter-trace-otlp-http": "0.35.0", + "@opentelemetry/resources": "1.9.0", + "@opentelemetry/sdk-trace-node": "1.9.0", + "@opentelemetry/semantic-conventions": "1.9.0", "chalk": "4.1.2", "check-node-version": "4.2.1", + "ci-info": "3.7.1", "core-js": "3.27.2", "enquirer": "2.3.6", + "envinfo": "7.8.1", "execa": "5.1.1", "fs-extra": "11.1.0", "listr2": "5.0.7", + "systeminformation": "5.17.4", "terminal-link": "2.1.1", "yargs": "17.6.2" }, diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js index 254bad0c70e9..eafaad0d1bb4 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -6,21 +6,8 @@ // Usage: // `$ yarn create redwood-app ./path/to/new-project` -import { spawn } from 'child_process' import path from 'path' -import chalk from 'chalk' -import checkNodeVersion from 'check-node-version' -import { prompt } from 'enquirer' -import execa from 'execa' -import fs from 'fs-extra' -import { Listr, figures } from 'listr2' -import terminalLink from 'terminal-link' -import { hideBin } from 'yargs/helpers' -import yargs from 'yargs/yargs' - -import { name, version } from '../package' - /** * To keep a consistent color/style palette between cli packages, such as * @redwood/create-redwood-app and @redwood/cli, please keep them compatible @@ -33,7 +20,29 @@ import { name, version } from '../package' * - packages/create-redwood-app/src/create-redwood-app.js (this file) * */ +import chalk from 'chalk' +import checkNodeVersion from 'check-node-version' +import { prompt } from 'enquirer' +import execa from 'execa' +import fs from 'fs-extra' +import { Listr, figures } from 'listr2' +import terminalLink from 'terminal-link' +import { hideBin, Parser } from 'yargs/helpers' +import yargs from 'yargs/yargs' + +import { name, version } from '../package' + +import { startTelemetry, shutdownTelemetry, startChildSpan } from './telemetry' ;(async () => { + // + + // Telemetry + const { telemetry } = Parser(hideBin(process.argv)) + if (telemetry !== 'false' && !process.env.REDWOOD_DISABLE_TELEMETRY) { + // Setup and start root span + await startTelemetry() + } + // Styles for terminal const style = { error: chalk.bold.red, @@ -68,7 +77,6 @@ import { name, version } from '../package' 'yarn-install': yarnInstall, typescript, overwrite, - telemetry: telemetry, yarn1, 'git-init': gitInit, } = yargs(hideBin(process.argv)) @@ -92,12 +100,6 @@ import { name, version } from '../package' type: 'boolean', describe: "Create even if target directory isn't empty", }) - .option('telemetry', { - default: true, - type: 'boolean', - describe: - 'Enables sending telemetry events for this create command and all Redwood CLI commands https://telemetry.redwoodjs.com', - }) .option('yarn1', { default: false, type: 'boolean', @@ -130,6 +132,9 @@ import { name, version } from '../package' 'my-redwood-app' )}` ) + await shutdownTelemetry({ + exception: new Error('no target directory specified'), + }) process.exit(1) } @@ -144,6 +149,7 @@ import { name, version } from '../package' appDirExists ? 'Using' : 'Creating' } directory '${newAppDir}'`, task: () => { + const span = startChildSpan('file_copy') if (appDirExists && !overwrite) { // make sure that the target directory is empty if (fs.readdirSync(newAppDir).length > 0) { @@ -152,7 +158,10 @@ import { name, version } from '../package' `\n'${newAppDir}' already exists and is not empty\n` ) ) - process.exit(1) + shutdownTelemetry({ + span, + exception: new Error('target directory not empty'), + }).finally(() => process.exit(1)) } } else { fs.ensureDirSync(path.dirname(newAppDir)) @@ -163,12 +172,14 @@ import { name, version } from '../package' path.join(newAppDir, 'gitignore.template'), path.join(newAppDir, '.gitignore') ) + span.end() }, }, { title: 'Converting to yarn 1', enabled: () => yarn1, task: () => { + const span = startChildSpan('yarn_1') // rm files: // - .yarnrc.yml // - .yarn @@ -196,6 +207,7 @@ import { name, version } from '../package' fs.writeJSONSync(path.join(newAppDir, 'package.json'), packageJSON, { spaces: 2, }) + span.end() }, }, ] @@ -210,52 +222,18 @@ import { name, version } from '../package' return 'skipped on request' } }, - task: () => { - return execa('yarn install', { + task: async () => { + const span = startChildSpan('yarn_install') + await execa('yarn install', { shell: true, cwd: newAppDir, }) + span.end() }, }, ] } - const sendTelemetry = ({ error } = {}) => { - // send 'create' telemetry event, or disable for new app - if (telemetry) { - const command = process.argv - // make command show 'create redwood-app [path] --flags' - command.splice(2, 0, 'create', 'redwood-app') - command[4] = '[path]' - - let args = [ - '--root', - newAppDir, - '--argv', - JSON.stringify(command), - '--duration', - Date.now() - startTime, - '--rwVersion', - version, - ] - if (error) { - args = [...args, '--error', `"${error}"`] - } - - spawn(process.execPath, [path.join(__dirname, 'telemetry.js'), ...args], { - detached: process.env.REDWOOD_VERBOSE_TELEMETRY ? false : true, - stdio: process.env.REDWOOD_VERBOSE_TELEMETRY ? 'inherit' : 'ignore', - }).unref() - } else { - fs.appendFileSync( - path.join(newAppDir, '.env'), - 'REDWOOD_DISABLE_TELEMETRY=1\n' - ) - } - } - - const startTime = Date.now() - // Engine check Listr. Separate Listr to avoid https://github.com/cenk1cenk2/listr2/issues/296 // Boolean flag let hasPassedEngineCheck = null @@ -327,10 +305,18 @@ import { name, version } from '../package' message: 'How would you like to proceed?', choices: ['Override error and continue install', 'Quit install'], initial: 0, - onCancel: () => process.exit(1), + onCancel: async () => { + await shutdownTelemetry({ + exception: new Error('cancelled engine override'), + }) + process.exit(1) + }, }) // Quit the install if user selects this option, otherwise it will proceed if (response['override-engine-error'] === 'Quit install') { + await shutdownTelemetry({ + exception: new Error('quit install at engine override'), + }) process.exit(1) } } @@ -392,34 +378,40 @@ import { name, version } from '../package' enabled: (ctx) => yarnInstall === true && (typescript === false || ctx.language === 'JavaScript'), - task: () => { - return execa('yarn rw ts-to-js', { + task: async () => { + const span = startChildSpan('ts_to_js') + await execa('yarn rw ts-to-js', { shell: true, cwd: newAppDir, }) + span.end() }, }, { title: 'Generating types', skip: () => yarnInstall === false, - task: () => { - return execa('yarn rw-gen', { + task: async () => { + const span = startChildSpan('generate_types') + await execa('yarn rw-gen', { shell: true, cwd: newAppDir, }) + span.end() }, }, { title: 'Initializing a git repo', enabled: (ctx) => gitInit || ctx.gitInit, - task: () => { - return execa( + task: async () => { + const span = startChildSpan('git_init') + await execa( 'git init && git add . && git commit -m "Initial commit"', { shell: true, cwd: newAppDir, } ) + span.end() }, }, ], @@ -430,8 +422,6 @@ import { name, version } from '../package' ) .run() .then(() => { - sendTelemetry() - // zOMG the semicolon below is a real Prettier thing. What?? // https://prettier.io/docs/en/rationale.html#semicolons ;[ @@ -482,11 +472,13 @@ import { name, version } from '../package' `${style.redwood(` > ${style.green(`yarn rw dev`)}`)}`, '', ].map((item) => console.log(item)) + shutdownTelemetry().then(() => { + process.exit(0) + }) }) .catch((e) => { console.log() console.log(e) - sendTelemetry({ error: e.message }) if (fs.existsSync(newAppDir)) { console.log( @@ -497,6 +489,6 @@ import { name, version } from '../package' ) ) } - process.exit(1) + shutdownTelemetry({ exception: e }).finally(() => process.exit(1)) }) })() diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js index e62de33587e8..3ad9c017689c 100644 --- a/packages/create-redwood-app/src/telemetry.js +++ b/packages/create-redwood-app/src/telemetry.js @@ -1,6 +1,146 @@ -const { sendTelemetry } = require('@redwoodjs/telemetry/dist/sendTelemetry') +import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api' +import opentelemetry, { SpanStatusCode } from '@opentelemetry/api' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' +import { Resource } from '@opentelemetry/resources' +import { + NodeTracerProvider, + BatchSpanProcessor, +} from '@opentelemetry/sdk-trace-node' +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' +import ci from 'ci-info' +import envinfo from 'envinfo' +import system from 'systeminformation' -// if this script is run directly by node then telemetry will be sent in immediately -;(async function () { - await sendTelemetry() -})() +import { name as packageName, version as packageVersion } from '../package' + +/** + * @type NodeTracerProvider + */ +let traceProvider + +/** + * @type BatchSpanProcessor + */ +let traceProcessor + +/** + * @type OTLPTraceExporter + */ +let traceExporter + +/** + * @type Span + */ +let rootSpan + +export async function startTelemetry() { + // Logger + // TODO: This line should be removed or at least the log level raised after experimentation + diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO) + + // Resources + const info = JSON.parse( + await envinfo.run( + { + System: ['OS', 'Shell'], + Binaries: ['Node', 'Yarn', 'npm'], + npmPackages: '@redwoodjs/*', + IDEs: ['VSCode'], + }, + { json: true } + ) + ) + + // get shell name instead of path + const shell = info.System?.Shell // Windows doesn't always provide shell info, I guess + if (shell?.path?.match('/')) { + info.System.Shell.name = info.System.Shell.path.split('/').pop() + } else if (shell?.path.match('\\')) { + info.System.Shell.name = info.System.Shell.path.split('\\').pop() + } + const cpu = await system.cpu() + const mem = await system.mem() + + const resource = Resource.default().merge( + new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: packageName, + [SemanticResourceAttributes.SERVICE_VERSION]: packageVersion, + [SemanticResourceAttributes.OS_TYPE]: info.System?.OS?.split(' ')[0], + [SemanticResourceAttributes.OS_VERSION]: info.System?.OS?.split(' ')[1], + 'shell.name': info.System?.Shell?.name, + 'node.version': info.Binaries?.Node?.version, + 'yarn.version': info.Binaries?.Yarn?.version, + 'npm.version': info.Binaries?.npm?.version, + 'vscode.version': info.IDEs?.VSCode?.version, + 'cpu.count': cpu.physicalCores, + 'memory.gb': Math.round(mem.total / 1073741824), + 'env.node_env': process.env.NODE_ENV || null, + 'ci.redwood': !!process.env.REDWOOD_CI, + 'ci.isci': ci.isCI, + fingerprint: undefined, // We don't provide a fingerprint here because it needs a fully setup project + }) + ) + + // Tracing + traceProvider = new NodeTracerProvider({ + resource: resource, + }) + traceExporter = new OTLPTraceExporter({ + // TODO: Point this to somewhere permanent + url: 'http://localhost:4318/v1/traces', + }) + traceProcessor = new BatchSpanProcessor(traceExporter) + traceProvider.addSpanProcessor(traceProcessor) + traceProvider.register() + + // Start root span + const tracer = opentelemetry.trace.getTracer('crwa-tracer') + rootSpan = tracer.startSpan( + 'root', + undefined, + opentelemetry.context.ROOT_CONTEXT + ) +} + +export function startChildSpan(name) { + const tracer = opentelemetry.trace.getTracer('crwa-tracer') + const childSpan = tracer.startSpan( + name, + undefined, + opentelemetry.trace.setSpan(opentelemetry.context.active(), rootSpan) + ) + return childSpan +} + +export async function shutdownTelemetry({ span, exception } = {}) { + if (span?.isRecording()) { + if (exception !== undefined) { + // TODO: Think about how best to redact this exception information + span.recordException(exception) + span.setStatus({ + code: SpanStatusCode.ERROR, + message: exception.message, + }) + } + span.end() + } + if (rootSpan?.isRecording()) { + if (span === undefined && exception !== undefined) { + // TODO: Think about how best to redact this exception information + rootSpan.recordException(exception) + rootSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: exception.message, + }) + } + rootSpan.end() + } + try { + await traceProvider?.shutdown() + await traceProcessor?.shutdown() + await traceExporter?.shutdown() + } catch (error) { + console.error('Telemetry error') + console.error(error) + } +} diff --git a/yarn.lock b/yarn.lock index 93b4529c7d66..a825b4f48e90 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6416,13 +6416,22 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.3.0": +"@opentelemetry/api@npm:1.4.0, @opentelemetry/api@npm:^1.3.0": version: 1.4.0 resolution: "@opentelemetry/api@npm:1.4.0" checksum: 1673fd2815eeb5487f2b0dec8595285e634f05b5b06b9a06db7af82238eac197b900ed10bebd923ac5d1fbc4da708e2f574aba58a465f27857f358c2d16987c3 languageName: node linkType: hard +"@opentelemetry/context-async-hooks@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/context-async-hooks@npm:1.9.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 639ff710015f2dd4c399e17fdcebf5fa1023eae533fcf84bd496050476429276a227700d05c3eabc5d54453267bb43ef788be7fff6897e16eadcf40b21c3ce21 + languageName: node + linkType: hard + "@opentelemetry/core@npm:1.9.0": version: 1.9.0 resolution: "@opentelemetry/core@npm:1.9.0" @@ -6434,6 +6443,68 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/exporter-trace-otlp-http@npm:0.35.0": + version: 0.35.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.35.0" + dependencies: + "@opentelemetry/core": 1.9.0 + "@opentelemetry/otlp-exporter-base": 0.35.0 + "@opentelemetry/otlp-transformer": 0.35.0 + "@opentelemetry/resources": 1.9.0 + "@opentelemetry/sdk-trace-base": 1.9.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: ce9621a3971f97c75211c639cde99adaddcde20fed6031171adc11b140466fc8f2dea42b74be0bbda357cc4b6c00ad5de636b86b6658d0b7fec2bbca5370f696 + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@npm:0.35.0": + version: 0.35.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.35.0" + dependencies: + "@opentelemetry/core": 1.9.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 747cddea28f923cb311928442d73e835f27e270784fdca4f260afa6764baede86dcce013deba56b9e09d69cde39ba5e6737a6a7c9be2838293e54f2c4202892d + languageName: node + linkType: hard + +"@opentelemetry/otlp-transformer@npm:0.35.0": + version: 0.35.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.35.0" + dependencies: + "@opentelemetry/core": 1.9.0 + "@opentelemetry/resources": 1.9.0 + "@opentelemetry/sdk-metrics": 1.9.0 + "@opentelemetry/sdk-trace-base": 1.9.0 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 4a03c7ea77c45e7a2677fd560ffb2253ea9e8a203600093ca2121d771e7292bc014ce7755b5d6926224badab09841318075b74147a82c9d286b3beb67ef304cb + languageName: node + linkType: hard + +"@opentelemetry/propagator-b3@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/propagator-b3@npm:1.9.0" + dependencies: + "@opentelemetry/core": 1.9.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 01aafd3551d52c7d85f28656d846587ee27457a6c7413d14a76a74ed451e90316acbb1f01ef37176a0325f3aed68cea7dff7866565d804ecba14fbb22ae667de + languageName: node + linkType: hard + +"@opentelemetry/propagator-jaeger@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/propagator-jaeger@npm:1.9.0" + dependencies: + "@opentelemetry/core": 1.9.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: ffea89dd14a29df01403219d847a0aefcbccbbf2f21889543e67e4b6d233ad75186a7fbf98916b53333470d717e486797a82a79711dc8530c2f158c5c2986c35 + languageName: node + linkType: hard + "@opentelemetry/resources@npm:1.9.0": version: 1.9.0 resolution: "@opentelemetry/resources@npm:1.9.0" @@ -6446,7 +6517,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:^1.8.0": +"@opentelemetry/sdk-metrics@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/sdk-metrics@npm:1.9.0" + dependencies: + "@opentelemetry/core": 1.9.0 + "@opentelemetry/resources": 1.9.0 + lodash.merge: 4.6.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 75a6fe9646eceee281342cc01f43d22df30ab887241bcc365241e3595f075fd147932fc9b7e70d950c7fdab679beb6649b5228f7be0cb34dcd07dc298211a16f + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:1.9.0, @opentelemetry/sdk-trace-base@npm:^1.8.0": version: 1.9.0 resolution: "@opentelemetry/sdk-trace-base@npm:1.9.0" dependencies: @@ -6459,6 +6543,22 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/sdk-trace-node@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/sdk-trace-node@npm:1.9.0" + dependencies: + "@opentelemetry/context-async-hooks": 1.9.0 + "@opentelemetry/core": 1.9.0 + "@opentelemetry/propagator-b3": 1.9.0 + "@opentelemetry/propagator-jaeger": 1.9.0 + "@opentelemetry/sdk-trace-base": 1.9.0 + semver: ^7.3.5 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: ee23f10e6f367127fbec149c03af5ff05848abb3e9f6fd8a4d3f1a95d80348c2eb936e24d51b2d5fa0bb5c1c71cdcb65d2407da3313de053f99bf5ca2cb0464c + languageName: node + linkType: hard + "@opentelemetry/semantic-conventions@npm:1.9.0": version: 1.9.0 resolution: "@opentelemetry/semantic-conventions@npm:1.9.0" @@ -15029,17 +15129,23 @@ __metadata: "@babel/core": 7.20.12 "@babel/node": 7.20.7 "@babel/runtime-corejs3": 7.20.13 - "@redwoodjs/internal": 3.2.0 - "@redwoodjs/telemetry": 3.2.0 + "@opentelemetry/api": 1.4.0 + "@opentelemetry/exporter-trace-otlp-http": 0.35.0 + "@opentelemetry/resources": 1.9.0 + "@opentelemetry/sdk-trace-node": 1.9.0 + "@opentelemetry/semantic-conventions": 1.9.0 "@types/prompts": 2.4.2 chalk: 4.1.2 check-node-version: 4.2.1 + ci-info: 3.7.1 core-js: 3.27.2 enquirer: 2.3.6 + envinfo: 7.8.1 execa: 5.1.1 fs-extra: 11.1.0 jest: 29.4.1 listr2: 5.0.7 + systeminformation: 5.17.4 terminal-link: 2.1.1 typescript: 4.7.4 yargs: 17.6.2