Skip to content

Commit

Permalink
Disable scarf-js analytics when yarn is the installing package manager (
Browse files Browse the repository at this point in the history
  • Loading branch information
aviaviavi authored May 8, 2020
1 parent 4c2a5cf commit 5fccab6
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 17 deletions.
57 changes: 40 additions & 17 deletions report.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ function dirName () {
return __dirname
}

function npmExecPath () {
return process.env.npm_execpath
}

const userMessageThrottleTime = 1000 * 60 // 1 minute
const execTimeout = 3000

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.'))
}

Expand All @@ -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-js is unable to inform user of analytics. Aborting.'))
}

await new Promise((resolve, reject) => {
if (dependencyInfo.parent.scarfSettings.defaultOptIn) {
if (userHasOptedOut(rootPackage)) {
Expand All @@ -153,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
Expand All @@ -172,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
Expand Down Expand Up @@ -404,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 => {
Expand All @@ -418,13 +451,3 @@ if (require.main === module) {
process.exit(0)
}
}

module.exports = {
redactSensitivePackageInfo,
hasHitRateLimit,
getRateLimitedLogHistory,
rateLimitedUserLog,
tmpFileName,
dirName,
processDependencyTreeOutput
}
43 changes: 43 additions & 0 deletions test/report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
}

0 comments on commit 5fccab6

Please sign in to comment.