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

feat(analytics): Add client-side functionality for analytics collection #5249

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions bin/scoop.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ switch ($subCommand) {
exec 'help' @($subCommand)
} else {
exec $subCommand $arguments

# Run analytics
$anltcs_time = get_config ANALYTICS_TIMESTAMP
if ($subCommand -ne 'analytics' -and
-not (get_config ANALYTICS_DISABLE) -and
-not (Get-CIEnvironment) -and
([String]::IsNullOrEmpty($anltcs_time) -or (New-TimeSpan $anltcs_time ([System.DateTime]::Now)).TotalDays -ge 7)) {
# Start-Process -NoNewWindow `
# -FilePath $(if ($PSEdition -eq 'Core') { 'pwsh.exe' } else { 'powershell.exe' }) `
# -ArgumentList "-NoProfile", "-File $PSScriptRoot\scoop.ps1", "analytics" `
# -RedirectStandardOutput "$Env:TEMP\scoop_analytics_out.txt" `
# -RedirectStandardError "$Env:TEMP\scoop_analytics_err.txt"
}
}
}
default {
Expand Down
28 changes: 28 additions & 0 deletions lib/core.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,34 @@ function Out-UTF8File {
}
}

function Get-CIEnvironment {
foreach ($ci_env in @{
'APPVEYOR' = 'AppVeyor'
'TF_BUILD' = 'Azure Pipelines'
'bamboo.buildKey' = 'Bamboo'
'BUILDKITE' = 'Buildkite'
'CIRCLECI' = 'Circle CI'
'CIRRUS_CI' = 'Cirrus CI'
'CODEBUILD_BUILD_ID' = 'CodeBuild'
'GITHUB_ACTIONS' = 'Github Actions'
'GITLAB_CI' = 'GitLab CI'
'HEROKU_TEST_RUN_ID' = 'Heroku CI'
'TEAMCITY_VERSION' = 'TeamCity'
'TRAVIS' = 'Travis CI'
}.GetEnumerator()) {
if (-not [String]::IsNullOrEmpty((Get-Item "Env:/$($ci_env.Key)" -ErrorAction Ignore).Value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (-not [String]::IsNullOrEmpty((Get-Item "Env:/$($ci_env.Key)" -ErrorAction Ignore).Value)) {
if ((Get-Item "Env:/$($ci_env.Key)" -ErrorAction Ignore).Value) {
❯ if ('') { $true } else { $false }
False

And why there's / after Env:?

return $ci_env.Value
}
}
foreach ($ci_env in 'BUILD_ID', 'CI') {
if (-not [String]::IsNullOrEmpty((Get-Item "Env:/$($ci_env)" -ErrorAction Ignore).Value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

return 'generic'
}
}

return ""
}

##################
# Core Bootstrap #
##################
Expand Down
128 changes: 128 additions & 0 deletions libexec/scoop-analytics.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Usage: scoop analytics
# Summary: Collects and sends Scoop usage analytics to a remote server
# Help: This is an internal command. It is used to collect and send usage analytics
# to a remote server, at an interval of 7 days. The following data is collected:
# - Randomly generated (one-time) anonymous ID
# - Machine info
# - OS build number
# - OS Architecture
# - PowerShell Desktop version
# - PowerShell Core version
# - Scoop version
# - Apps installed from public buckets (private apps are filtered out)
# - Name
# - Version
# - Last updated
# - Source
# - Architecture
# - User or Global installation
# - Installation status
# - Public buckets (private buckets are filtered out)
# - Name
# - Source
# - Last updated
# - Manifest count

. "$PSScriptRoot\..\lib\json.ps1" # 'ConvertToPrettyJson'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if(!(Resolve-DnsName analytics.scoop.sh -ErrorAction SilentlyContinue)) {
Write-Host "Could not resolve analytics.scoop.sh"
exit 0
}

Just skip the whole script if analytics.scoop.sh can't be resolved (because it will get added to public blocklists 😄)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, good idea. But I notice that it takes 10 seconds to time out, isn't that a bit much?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it returns instantly for me. Maybe because I use a local Adguard in my network. 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that it takes time when connected to my Azure VPN server. On other networks it returns instantly.

It's useful regardless, so I'll add it.

if ([String]::IsNullOrEmpty((get_config ANALYTICS_ID))) {
set_config ANALYTICS_ID (New-Guid).Guid | Out-Null
}
Comment on lines +28 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use random ID is better IMO

$def_arch = Get-DefaultArchitecture
$known_sources = foreach ($item in (known_bucket_repos).PSObject.Properties) { $item.Value }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$known_sources = foreach ($item in (known_bucket_repos).PSObject.Properties) { $item.Value }


function Test-PublicSource($source) {
# Known sources
if ($source -in $known_sources) {
return $true
}
# Local file paths, SSH remotes, and remotes with usernames
if ($source -match '^/[A-Za-z]:/|^[A-Za-z]:/|^\./|^\.\./|file:/|ssh:/|@') {
return $false
}
return $true
Comment on lines +35 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Known sources
if ($source -in $known_sources) {
return $true
}
# Local file paths, SSH remotes, and remotes with usernames
if ($source -match '^/[A-Za-z]:/|^[A-Za-z]:/|^\./|^\.\./|file:/|ssh:/|@') {
return $false
}
return $true
# Local file paths, SSH remotes, and remotes with usernames
if ($source -match '^/[A-Za-z]:/|^[A-Za-z]:/|^\./|^\.\./|file:/|ssh:/|@') {
return $false
} else {
return $true
}

}


$stats = [ordered]@{}
$stats.id = get_config ANALYTICS_ID

$stats.machine = [ordered]@{
Build = [System.Environment]::OSVersion.Version.ToString()
Arch = $def_arch
Desktop = (Get-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine -Name 'PowerShellVersion').PowerShellVersion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this useful?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that information is also available in $PSVersionTable

Core = if (Get-Command pwsh -ErrorAction Ignore) {
(Get-Item (Get-Command pwsh).Source).VersionInfo.ProductVersionRaw.ToString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(Get-Item (Get-Command pwsh).Source).VersionInfo.ProductVersionRaw.ToString()
(Get-Command pwsh).Version.ToString()

} else {
""
}
Scoop = if (Test-Path "$PSScriptRoot\..\.git") {
$branch = (Get-Content "$PSScriptRoot\..\.git\HEAD").Replace('ref: ', '')
"$(Get-Content (Join-Path "$PSScriptRoot\..\.git" $branch)) ($($branch.Split('/')[-1]))"
} elseif (Test-Path "$PSScriptRoot\..\CHANGELOG.md") {
(Select-String '^## .*([\d]{4}-[\d]{2}-[\d]{2})' "$PSScriptRoot\..\CHANGELOG.md").Matches.Groups[1].Value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(Select-String '^## .*([\d]{4}-[\d]{2}-[\d]{2})' "$PSScriptRoot\..\CHANGELOG.md").Matches.Groups[1].Value
(Select-String -Pattern '^## \[v([\d.]+)\].*?([\d-]+)$' -Path "$PSScriptRoot\..\CHANGELOG.md").Matches.Groups[1].Value

The version number is better. I don't think there's user used v0.1.0 and lower.

} else {
""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, the CHANGELOG.md should be there, so this will be never reached.

}
}

$bucket_names = @()
$stats.buckets = @()
foreach ($item in list_buckets) {
# Filter out private buckets
if (Test-PublicSource $item.Source) {
$stats.buckets += $item
$bucket_names += $item.Name
}
}

$stats.apps = @()
foreach ($item in @(& "$PSScriptRoot\scoop-list.ps1" 6>$null)) {
# Filter out private apps
if ($item.Source -notin $bucket_names) {
continue
}

$info = $item.Info -Split ', '

$newitem = [ordered]@{}
$newitem.Name = $item.Name
$newitem.Version = $item.Version
$newitem.Source = $item.Source
$newitem.Updated = $item.Updated
$newitem.Global = 'Global install' -in $info
$newitem.Arch = if ('64bit' -in $info -and '64bit' -ne $def_arch) {
'64bit'
} elseif ('32bit' -in $info -and '32bit' -ne $def_arch) {
'32bit'
} elseif ('arm64' -in $info -and 'arm64' -ne $def_arch) {
'arm64'
} else {
$def_arch
}
$newitem.Status = if ('Held package' -in $info) {
'Held'
} elseif ('Install failed' -in $info) {
'Failed'
} else {
'OK'
}

$stats.apps += $newitem
}

$payload = $stats | ConvertToPrettyJSON
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload needn't be prettified, right?


try {
Invoke-RestMethod -Method Post `
-Uri 'https://analytics.scoop.sh/post' `
-Body $payload `
-ContentType "application/json"
set_config ANALYTICS_TIMESTAMP ([System.DateTime]::Now.ToString('o')) | Out-Null
} catch {
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
}

exit 0

3 changes: 3 additions & 0 deletions libexec/scoop-config.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
# Nightly version is formatted as 'nightly-yyyyMMdd' and will be updated after one day if this is set to $true.
# Otherwise, nightly version will not be updated unless `--force` is used.
#
# analytics_disable: $true|$false
# Set this to $true to disable sending anonymous usage analytics.
#
# ARIA2 configuration
# -------------------
#
Expand Down