From 41de2d6774dde263938507d7bfb202784cb069a9 Mon Sep 17 00:00:00 2001 From: aviaviavi Date: Thu, 7 May 2020 17:11:17 -0700 Subject: [PATCH 1/4] Disable scarf analytics when yarn is the installing package manager --- report.js | 35 +++++++++++++++++++++++++++++------ test/report.test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/report.js b/report.js index 52569bb..0589fbf 100644 --- a/report.js +++ b/report.js @@ -22,6 +22,10 @@ function dirName () { return __dirname } +function npmExecPath () { + return process.env.npm_execpath +} + const userMessageThrottleTime = 1000 * 60 // 1 minute const execTimeout = 3000 @@ -76,10 +80,21 @@ function redactSensitivePackageInfo (dependencyInfo) { return dependencyInfo } +/* + Scarf-js is automatically disabled when being run inside of a yarn install. + The `npm_execpath` environment variable tells us which package manager is + running our install + */ +function isYarn () { + const execPath = module.exports.npmExecPath() || '' + return ['yarn', 'yarn.js', 'yarnpkg', 'yarn.cmd', 'yarnpkg.cmd'] + .some(packageManBinName => execPath.endsWith(packageManBinName)) +} + function processDependencyTreeOutput (resolve, reject) { return function (error, stdout, stderr) { - if (error) { - return reject(new Error(`Scarf received an error from npm -ls: ${error}`)) + if (error && !stdout) { + return reject(new Error(`Scarf received an error from npm -ls: ${error} | ${stderr}`)) } try { @@ -116,8 +131,8 @@ function processDependencyTreeOutput (resolve, reject) { } // If any intermediate dependency in the chain of deps that leads to scarf - // has disabled Scarf, we must respect that setting - if (dependencyToReport.anyInChainDisabled) { + // has disabled Scarf, we must respect that setting unless the user overrides it. + if (dependencyToReport.anyInChainDisabled && !userHasOptedIn(dependencyToReport.rootPackage)) { return reject(new Error('Scarf has been disabled via a package.json in the dependency chain.')) } @@ -137,13 +152,18 @@ async function getDependencyInfo () { async function reportPostInstall () { const scarfApiToken = process.env.SCARF_API_TOKEN - const dependencyInfo = await getDependencyInfo() + + const dependencyInfo = await module.exports.getDependencyInfo() if (!dependencyInfo.parent || !dependencyInfo.parent.name) { return Promise.reject(new Error('No parent, nothing to report')) } const rootPackage = dependencyInfo.rootPackage + if (!userHasOptedIn(rootPackage) && isYarn()) { + return Promise.reject(new Error('Package manager is yarn, Scarf is unable to inform user of analytics. Aborting.')) + } + await new Promise((resolve, reject) => { if (dependencyInfo.parent.scarfSettings.defaultOptIn) { if (userHasOptedOut(rootPackage)) { @@ -426,5 +446,8 @@ module.exports = { rateLimitedUserLog, tmpFileName, dirName, - processDependencyTreeOutput + processDependencyTreeOutput, + npmExecPath, + getDependencyInfo, + reportPostInstall } diff --git a/test/report.test.js b/test/report.test.js index 63eb36b..2f3e74c 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -70,9 +70,11 @@ describe('Reporting tests', () => { test('Intermediate packages can disable Scarf for their dependents', async () => { const exampleLsOutput = fs.readFileSync('./test/example-ls-output.json') + await expect(new Promise((resolve, reject) => { return report.processDependencyTreeOutput(resolve, reject)(null, exampleLsOutput, null) })).rejects.toEqual(new Error('Scarf has been disabled via a package.json in the dependency chain.')) + const parsedLsOutput = JSON.parse(exampleLsOutput) delete (parsedLsOutput.dependencies['scarfed-lib-consumer'].scarfSettings) @@ -83,4 +85,45 @@ describe('Reporting tests', () => { expect(output.anyInChainDisabled).toBe(false) }) }) + + test('Disable when package manager is yarn', async () => { + const parsedLsOutput = dependencyTreeScarfEnabled() + + await new Promise((resolve, reject) => { + return report.processDependencyTreeOutput(resolve, reject)(null, JSON.stringify(parsedLsOutput), null) + }).then(output => { + expect(output).toBeTruthy() + expect(output.anyInChainDisabled).toBe(false) + }) + + // Simulate a yarn install by mocking the env variable npm_execpath + // leading to a yarn executable + report.npmExecPath = jest.fn(() => { + return '/usr/local/lib/node_modules/yarn/bin/yarn.js' + }) + + report.getDependencyInfo = jest.fn(() => { + return Promise.resolve({ + scarf: { name: '@scarf/scarf', version: '0.0.1' }, + parent: { name: 'scarfed-library', version: '1.0.0', scarfSettings: { defaultOptIn: true } }, + grandparent: { name: 'scarfed-lib-consumer', version: '1.0.0' } + }) + }) + + try { + await report.reportPostInstall() + throw new Error("report.reportPostInstall() didn't throw an error") + } catch (err) { + expect(err.message).toContain('yarn') + } + }) }) + +function dependencyTreeScarfEnabled () { + const exampleLsOutput = fs.readFileSync('./test/example-ls-output.json') + + const parsedLsOutput = JSON.parse(exampleLsOutput) + delete (parsedLsOutput.dependencies['scarfed-lib-consumer'].scarfSettings) + + return parsedLsOutput +} From 5a2b36a90e7ff29b11da29c9438f3ac07cfd18f0 Mon Sep 17 00:00:00 2001 From: aviaviavi Date: Fri, 8 May 2020 10:05:53 -0700 Subject: [PATCH 2/4] move module.exports above main invocation --- report.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/report.js b/report.js index 0589fbf..87a905f 100644 --- a/report.js +++ b/report.js @@ -424,6 +424,19 @@ function writeCurrentTimeToLogHistory (rateLimitKey, history) { fs.writeFileSync(module.exports.tmpFileName(), JSON.stringify(history)) } +module.exports = { + redactSensitivePackageInfo, + hasHitRateLimit, + getRateLimitedLogHistory, + rateLimitedUserLog, + tmpFileName, + dirName, + processDependencyTreeOutput, + npmExecPath, + getDependencyInfo, + reportPostInstall, +} + if (require.main === module) { try { reportPostInstall().catch(e => { @@ -439,15 +452,3 @@ if (require.main === module) { } } -module.exports = { - redactSensitivePackageInfo, - hasHitRateLimit, - getRateLimitedLogHistory, - rateLimitedUserLog, - tmpFileName, - dirName, - processDependencyTreeOutput, - npmExecPath, - getDependencyInfo, - reportPostInstall -} From 3303b5e9e3119b58019c21a44576050e84575cb7 Mon Sep 17 00:00:00 2001 From: aviaviavi Date: Fri, 8 May 2020 10:06:55 -0700 Subject: [PATCH 3/4] standard --- report.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/report.js b/report.js index 87a905f..137e5c8 100644 --- a/report.js +++ b/report.js @@ -434,7 +434,7 @@ module.exports = { processDependencyTreeOutput, npmExecPath, getDependencyInfo, - reportPostInstall, + reportPostInstall } if (require.main === module) { @@ -451,4 +451,3 @@ if (require.main === module) { process.exit(0) } } - From 3775f0a47c54e16eb5eff4e53c6bf87dfa263789 Mon Sep 17 00:00:00 2001 From: aviaviavi Date: Fri, 8 May 2020 10:20:45 -0700 Subject: [PATCH 4/4] s/Scarf/scarf-js/g --- report.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/report.js b/report.js index 137e5c8..a092726 100644 --- a/report.js +++ b/report.js @@ -161,7 +161,7 @@ async function reportPostInstall () { const rootPackage = dependencyInfo.rootPackage if (!userHasOptedIn(rootPackage) && isYarn()) { - return Promise.reject(new Error('Package manager is yarn, Scarf is unable to inform user of analytics. Aborting.')) + return Promise.reject(new Error('Package manager is yarn. scarf-js is unable to inform user of analytics. Aborting.')) } await new Promise((resolve, reject) => { @@ -173,7 +173,7 @@ async function reportPostInstall () { if (!userHasOptedIn(rootPackage)) { rateLimitedUserLog(optedInLogRateLimitKey, ` The dependency '${dependencyInfo.parent.name}' is tracking installation - statistics using Scarf (https://scarf.sh), which helps open-source developers + statistics using scarf-js (https://scarf.sh), which helps open-source developers fund and maintain their projects. Scarf securely logs basic installation details when this package is installed. The Scarf npm library is open source and permissively licensed at https://github.com/scarf-sh/scarf-js. For more @@ -192,7 +192,7 @@ async function reportPostInstall () { } rateLimitedUserLog(optedOutLogRateLimitKey, ` The dependency '${dependencyInfo.parent.name}' would like to track - installation statistics using Scarf (https://scarf.sh), which helps + installation statistics using scarf-js (https://scarf.sh), which helps open-source developers fund and maintain their projects. Reporting is disabled by default for this package. When enabled, Scarf securely logs basic installation details when this package is installed. The Scarf npm library is