Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable scarf-js analytics when yarn is the installing package manager #21

Merged
merged 4 commits into from
May 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}