Skip to content

Commit 5fccab6

Browse files
authored
Disable scarf-js analytics when yarn is the installing package manager (#21)
1 parent 4c2a5cf commit 5fccab6

File tree

2 files changed

+83
-17
lines changed

2 files changed

+83
-17
lines changed

report.js

+40-17
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ function dirName () {
2222
return __dirname
2323
}
2424

25+
function npmExecPath () {
26+
return process.env.npm_execpath
27+
}
28+
2529
const userMessageThrottleTime = 1000 * 60 // 1 minute
2630
const execTimeout = 3000
2731

@@ -76,10 +80,21 @@ function redactSensitivePackageInfo (dependencyInfo) {
7680
return dependencyInfo
7781
}
7882

83+
/*
84+
Scarf-js is automatically disabled when being run inside of a yarn install.
85+
The `npm_execpath` environment variable tells us which package manager is
86+
running our install
87+
*/
88+
function isYarn () {
89+
const execPath = module.exports.npmExecPath() || ''
90+
return ['yarn', 'yarn.js', 'yarnpkg', 'yarn.cmd', 'yarnpkg.cmd']
91+
.some(packageManBinName => execPath.endsWith(packageManBinName))
92+
}
93+
7994
function processDependencyTreeOutput (resolve, reject) {
8095
return function (error, stdout, stderr) {
81-
if (error) {
82-
return reject(new Error(`Scarf received an error from npm -ls: ${error}`))
96+
if (error && !stdout) {
97+
return reject(new Error(`Scarf received an error from npm -ls: ${error} | ${stderr}`))
8398
}
8499

85100
try {
@@ -116,8 +131,8 @@ function processDependencyTreeOutput (resolve, reject) {
116131
}
117132

118133
// If any intermediate dependency in the chain of deps that leads to scarf
119-
// has disabled Scarf, we must respect that setting
120-
if (dependencyToReport.anyInChainDisabled) {
134+
// has disabled Scarf, we must respect that setting unless the user overrides it.
135+
if (dependencyToReport.anyInChainDisabled && !userHasOptedIn(dependencyToReport.rootPackage)) {
121136
return reject(new Error('Scarf has been disabled via a package.json in the dependency chain.'))
122137
}
123138

@@ -137,13 +152,18 @@ async function getDependencyInfo () {
137152

138153
async function reportPostInstall () {
139154
const scarfApiToken = process.env.SCARF_API_TOKEN
140-
const dependencyInfo = await getDependencyInfo()
155+
156+
const dependencyInfo = await module.exports.getDependencyInfo()
141157
if (!dependencyInfo.parent || !dependencyInfo.parent.name) {
142158
return Promise.reject(new Error('No parent, nothing to report'))
143159
}
144160

145161
const rootPackage = dependencyInfo.rootPackage
146162

163+
if (!userHasOptedIn(rootPackage) && isYarn()) {
164+
return Promise.reject(new Error('Package manager is yarn. scarf-js is unable to inform user of analytics. Aborting.'))
165+
}
166+
147167
await new Promise((resolve, reject) => {
148168
if (dependencyInfo.parent.scarfSettings.defaultOptIn) {
149169
if (userHasOptedOut(rootPackage)) {
@@ -153,7 +173,7 @@ async function reportPostInstall () {
153173
if (!userHasOptedIn(rootPackage)) {
154174
rateLimitedUserLog(optedInLogRateLimitKey, `
155175
The dependency '${dependencyInfo.parent.name}' is tracking installation
156-
statistics using Scarf (https://scarf.sh), which helps open-source developers
176+
statistics using scarf-js (https://scarf.sh), which helps open-source developers
157177
fund and maintain their projects. Scarf securely logs basic installation
158178
details when this package is installed. The Scarf npm library is open source
159179
and permissively licensed at https://github.com/scarf-sh/scarf-js. For more
@@ -172,7 +192,7 @@ async function reportPostInstall () {
172192
}
173193
rateLimitedUserLog(optedOutLogRateLimitKey, `
174194
The dependency '${dependencyInfo.parent.name}' would like to track
175-
installation statistics using Scarf (https://scarf.sh), which helps
195+
installation statistics using scarf-js (https://scarf.sh), which helps
176196
open-source developers fund and maintain their projects. Reporting is disabled
177197
by default for this package. When enabled, Scarf securely logs basic
178198
installation details when this package is installed. The Scarf npm library is
@@ -404,6 +424,19 @@ function writeCurrentTimeToLogHistory (rateLimitKey, history) {
404424
fs.writeFileSync(module.exports.tmpFileName(), JSON.stringify(history))
405425
}
406426

427+
module.exports = {
428+
redactSensitivePackageInfo,
429+
hasHitRateLimit,
430+
getRateLimitedLogHistory,
431+
rateLimitedUserLog,
432+
tmpFileName,
433+
dirName,
434+
processDependencyTreeOutput,
435+
npmExecPath,
436+
getDependencyInfo,
437+
reportPostInstall
438+
}
439+
407440
if (require.main === module) {
408441
try {
409442
reportPostInstall().catch(e => {
@@ -418,13 +451,3 @@ if (require.main === module) {
418451
process.exit(0)
419452
}
420453
}
421-
422-
module.exports = {
423-
redactSensitivePackageInfo,
424-
hasHitRateLimit,
425-
getRateLimitedLogHistory,
426-
rateLimitedUserLog,
427-
tmpFileName,
428-
dirName,
429-
processDependencyTreeOutput
430-
}

test/report.test.js

+43
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ describe('Reporting tests', () => {
7070

7171
test('Intermediate packages can disable Scarf for their dependents', async () => {
7272
const exampleLsOutput = fs.readFileSync('./test/example-ls-output.json')
73+
7374
await expect(new Promise((resolve, reject) => {
7475
return report.processDependencyTreeOutput(resolve, reject)(null, exampleLsOutput, null)
7576
})).rejects.toEqual(new Error('Scarf has been disabled via a package.json in the dependency chain.'))
77+
7678
const parsedLsOutput = JSON.parse(exampleLsOutput)
7779
delete (parsedLsOutput.dependencies['scarfed-lib-consumer'].scarfSettings)
7880

@@ -83,4 +85,45 @@ describe('Reporting tests', () => {
8385
expect(output.anyInChainDisabled).toBe(false)
8486
})
8587
})
88+
89+
test('Disable when package manager is yarn', async () => {
90+
const parsedLsOutput = dependencyTreeScarfEnabled()
91+
92+
await new Promise((resolve, reject) => {
93+
return report.processDependencyTreeOutput(resolve, reject)(null, JSON.stringify(parsedLsOutput), null)
94+
}).then(output => {
95+
expect(output).toBeTruthy()
96+
expect(output.anyInChainDisabled).toBe(false)
97+
})
98+
99+
// Simulate a yarn install by mocking the env variable npm_execpath
100+
// leading to a yarn executable
101+
report.npmExecPath = jest.fn(() => {
102+
return '/usr/local/lib/node_modules/yarn/bin/yarn.js'
103+
})
104+
105+
report.getDependencyInfo = jest.fn(() => {
106+
return Promise.resolve({
107+
scarf: { name: '@scarf/scarf', version: '0.0.1' },
108+
parent: { name: 'scarfed-library', version: '1.0.0', scarfSettings: { defaultOptIn: true } },
109+
grandparent: { name: 'scarfed-lib-consumer', version: '1.0.0' }
110+
})
111+
})
112+
113+
try {
114+
await report.reportPostInstall()
115+
throw new Error("report.reportPostInstall() didn't throw an error")
116+
} catch (err) {
117+
expect(err.message).toContain('yarn')
118+
}
119+
})
86120
})
121+
122+
function dependencyTreeScarfEnabled () {
123+
const exampleLsOutput = fs.readFileSync('./test/example-ls-output.json')
124+
125+
const parsedLsOutput = JSON.parse(exampleLsOutput)
126+
delete (parsedLsOutput.dependencies['scarfed-lib-consumer'].scarfSettings)
127+
128+
return parsedLsOutput
129+
}

0 commit comments

Comments
 (0)