├── .gitignore
├── .npmrc
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── logo.svg
├── package-lock.json
├── package.json
├── src
├── caniuse-agent-data.js
├── caniuse-parser.js
├── google-analytics.js
├── google-auth.js
└── listen.js
└── tests
├── caniuse-agent-data.test.js
└── caniuse-parser.test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Dependency directories
11 | /node_modules
12 |
13 | # Yarn Integrity file
14 | .yarn-integrity
15 |
16 | # dotenv environment variables file
17 | .env
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | # Misc
24 | .DS_Store
25 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | - "8"
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 David Francisco
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Target browsers tailored to your audience.
4 |
5 | ---
6 |
7 |
8 | Interested in a bot that does all of this for you? 9 | Click here! 10 | 11 |
12 | 13 | --- 14 | 15 | ## How to use 16 | 17 | In the root directory of your project run: 18 | 19 | ```yaml 20 | npx browserslist-ga 21 | ``` 22 | 23 | _([npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) comes with npm 5.2+, for older versions run `npm install -g browserslist-ga` and then `browserslist-ga`)_ 24 | _(to run the latest code directly from GitHub, execute `npx github:browserslist/browserslist-ga` instead)_ 25 | 26 | You'll be asked to login with your Google Account (please see [this issue](https://github.com/browserslist/browserslist-ga/issues/26#issuecomment-568583144) if you are unable to sign in). Your access token will only be used locally to generate a `browserslist-stats.json` file in the root of your project. After finishing the steps, you can use your stats with Browserlist by adding the following to your [Browserslist config](https://github.com/ai/browserslist#config-file): 27 | 28 | ```yaml 29 | > 0.5% in my stats # Or a different percentage 30 | ``` 31 | 32 | Note that you can query against your custom usage data while also querying against global or regional data. For example, the query `> 1% in my stats, > 5% in US, 10%` is permitted. 33 | 34 | ## Why should I care? 35 | 36 | Browsers update very often these days, with major releases getting published every month. 37 | With each new browser version comes support for new web platform features. 38 | Thanks to open source projects such as Autoprefixer and Babel we are able to use these features while supporting older browsers. 39 | But this backward compatibility comes with a cost. 40 | We can't really keep adding prefixes, polyfills and other fallbacks to support every browser ever invented. 41 | 42 | Browserslist is an open source project that can minimize those costs by allowing you to configure which browsers you care about. 43 | It is supported by tools such as 44 | [Autoprefixer](https://github.com/postcss/autoprefixer), 45 | [babel-preset-env](https://github.com/babel/babel/tree/master/packages/babel-preset-env), 46 | [postcss-normalize](https://github.com/jonathantneal/postcss-normalize) and many others. 47 | Here's how you configure Browserslist: 48 | 49 | ```yaml 50 | > 1% # I want to support browser versions that have more than 1% of global usage 51 | Last 2 versions # And the latest 2 versions of each browser 52 | IE 9 # And also Internet Explorer 9 specifically 53 | ``` 54 | 55 | The global browser usage data comes from [caniuse.com](https://caniuse.com) and is downloaded from npm when you run `npm install`. 56 | Package managers such as npm and Yarn will generate a lockfile with the exact version of each package that was installed. 57 | This means the caniuse database that is used to perform these queries will always be the same. 58 | This is great because it's predictable, but it's important to update this package from time to time to keep up with the latest stats. 59 | Apart from remembering to update this package, there's something else you should consider: 60 | 61 | * For instance, in China there are some popular browsers that are not used in the US and Europe. 62 | * Or maybe your audience uses mostly mobile browsers. 63 | * Or maybe you are building an application for the government and need to support Internet Explorer 8. 64 | 65 | The point being, it's important to make decisions based on your audience. 66 | Browserslist-GA aims to help you with that. 67 | It integrates Google Analytics with Browserslist to keep your targeted browsers updated. 68 | 69 | ## Notes 70 | 71 | There are some differences compared to the `caniuse` Google Analytics importer: 72 | 73 | * All browsers on iOS use Safari's WebKit as the underlying engine, and hence will be resolved to Safari. The `caniuse` Google Analytics parser only converts some of the data to Safari, while the remaining is left untracked (see [#1](https://github.com/browserslist/browserslist-ga/pull/1)). 74 | * YaBrowser, a popular browser in russian-speaking countries, uses the Blink web browser engine and is based on Chromium. It is currently not available on `caniuse` and so is resolved to Chrome (or Chrome for Android) and the version is mapped to the nearest available version (see [#2](https://github.com/browserslist/browserslist-ga/pull/2)). 75 | * Just like for YaBrowser, the [same approach](https://github.com/dmfrancisco/map-to-chrome/blob/master/data/coc_coc_browser.json) is applied to the Coc Coc browser. 76 | 77 | ## Kudos 78 | 79 | All the praise goes to the humans and martians that develop and maintain [Can I Use](https://caniuse.com) and [Browserslist](https://github.com/ai/browserslist). 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | const inquirer = require("inquirer"); 5 | const googleAuth = require("./src/google-auth"); 6 | const { getAccounts, getWebProperties, getProfiles, getData } = require("./src/google-analytics"); 7 | const { parse } = require("./src/caniuse-parser"); 8 | 9 | inquirer.registerPrompt("datetime", require("inquirer-datepicker-prompt")); 10 | 11 | const outputFilename = "browserslist-stats.json"; 12 | 13 | googleAuth(oauth2Client => { 14 | let selectedProfile; 15 | 16 | getAccounts(oauth2Client) 17 | .then((accounts) => { 18 | if (accounts.length === 0) { 19 | throw new Error('No Google Analytics accounts.') 20 | } 21 | 22 | return accounts; 23 | }) 24 | .then(accounts => 25 | inquirer.prompt([ 26 | { 27 | type: "list", 28 | name: "account", 29 | message: "Please select an account:", 30 | choices: accounts.map(account => ({ 31 | value: account, 32 | name: `${account.name} (#${account.id})`, 33 | })), 34 | }, 35 | ]) 36 | ) 37 | .then(({ account }) => getWebProperties(oauth2Client, account.id)) 38 | .then(webProperties => 39 | inquirer.prompt([ 40 | { 41 | type: "list", 42 | name: "webProperty", 43 | message: "Please select a property:", 44 | choices: webProperties.map(webProperty => ({ 45 | value: webProperty, 46 | name: `${webProperty.name} (#${webProperty.id})`, 47 | })), 48 | }, 49 | ]) 50 | ) 51 | .then(({ webProperty }) => getProfiles(oauth2Client, webProperty.accountId, webProperty.id)) 52 | .then(profiles => 53 | inquirer.prompt([ 54 | { 55 | type: "list", 56 | name: "profile", 57 | message: "Please select a profile:", 58 | choices: profiles.map(profile => ({ 59 | value: profile, 60 | name: `${profile.name} (#${profile.id})`, 61 | })), 62 | }, 63 | ]) 64 | ) 65 | .then(({ profile }) => { 66 | const defaultStartDate = new Date(); 67 | const defaultEndDate = new Date(); 68 | 69 | selectedProfile = profile; 70 | 71 | // End date defaults to today, start date defaults to 90 days ago 72 | defaultStartDate.setDate(defaultEndDate.getDate() - 90); 73 | 74 | return inquirer.prompt([ 75 | { 76 | type: "datetime", 77 | name: "startDate", 78 | message: 'Specify a start date (format is "YYYY-MM-DD", defaults to 90 days ago):', 79 | format: ["yyyy", "-", "mm", "-", "dd"], 80 | initial: defaultStartDate, 81 | }, 82 | { 83 | type: "datetime", 84 | name: "endDate", 85 | message: 'Specify an end date (format is "YYYY-MM-DD", defaults to today):', 86 | format: ["yyyy", "-", "mm", "-", "dd"], 87 | initial: defaultEndDate, 88 | }, 89 | ]); 90 | }) 91 | .then(({ startDate, endDate }) => getData(oauth2Client, selectedProfile.id, startDate, endDate)) 92 | .then(parse) 93 | .then(stats => { 94 | fs.writeFileSync(outputFilename, JSON.stringify(stats, null, 2)); 95 | console.log(`Success! Stats saved to '${outputFilename}'`); 96 | process.exit(); 97 | }) 98 | .catch(err => { 99 | console.error(err); 100 | process.exit(1); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserslist-ga", 3 | "version": "0.0.13", 4 | "description": "Target browsers tailored to your audience", 5 | "repository": "github:browserslist/browserslist-ga", 6 | "license": "MIT", 7 | "bin": { 8 | "browserslist-ga": "index.js" 9 | }, 10 | "engines": { 11 | "node": ">= 8.0.0" 12 | }, 13 | "scripts": { 14 | "precommit": "lint-staged", 15 | "test": "jest" 16 | }, 17 | "dependencies": { 18 | "caniuse-lite": "^1.0.30001016", 19 | "googleapis": "^39.1.0", 20 | "inquirer": "^5.2.0", 21 | "inquirer-datepicker-prompt": "^0.4.2", 22 | "map-to-chrome": "0.0.3", 23 | "opener": "^1.5.1", 24 | "portfinder": "^1.0.25" 25 | }, 26 | "prettier": { 27 | "printWidth": 100, 28 | "trailingComma": "es5" 29 | }, 30 | "lint-staged": { 31 | "src/**/*.js": [ 32 | "prettier --write", 33 | "git add" 34 | ] 35 | }, 36 | "eslintConfig": { 37 | "extends": "eslint:recommended", 38 | "env": { 39 | "node": true, 40 | "es6": true 41 | }, 42 | "parserOptions": { 43 | "ecmaVersion": 6 44 | }, 45 | "rules": { 46 | "no-console": "off" 47 | }, 48 | "globals": { 49 | "expect": false, 50 | "test": false 51 | } 52 | }, 53 | "jest": { 54 | "testURL": "http://localhost/" 55 | }, 56 | "devDependencies": { 57 | "husky": "^0.14.3", 58 | "jest": "^22.4.3", 59 | "lint-staged": "^7.0.5", 60 | "prettier": "^1.19.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/caniuse-agent-data.js: -------------------------------------------------------------------------------- 1 | /* Use data from `caniuse-lite` and convert to format used on caniuse.com */ 2 | 3 | const { agents } = require("caniuse-lite/dist/unpacker/agents"); 4 | 5 | const agentData = {}; 6 | 7 | Object.entries(agents).forEach(([browserCode, data]) => { 8 | const versions = data.versions.filter(version => version !== null); 9 | 10 | // Sort versions by timestamp. Unreleased versions have null timestamp, so they will be at the end 11 | const versionsByDate = Object.entries(data.release_date).sort((a, b) => b[1] - a[1]); 12 | 13 | agentData[browserCode] = { 14 | version_list: versions.map(version => ({ version })), 15 | current_version: versionsByDate[0][0], 16 | browser: data.browser, 17 | }; 18 | }); 19 | 20 | module.exports = agentData; 21 | -------------------------------------------------------------------------------- /src/caniuse-parser.js: -------------------------------------------------------------------------------- 1 | /* Code adapted from caniuse.com with permission */ 2 | 3 | var yaBrowserMapping = require("map-to-chrome/data/YaBrowser.json"); 4 | var cocCocMapping = require("map-to-chrome/data/coc_coc_browser.json"); 5 | var agentData = require("./caniuse-agent-data"); 6 | 7 | var helpers = {}; 8 | var versionCache = {}; 9 | var browsers; 10 | 11 | var OPERA_MINI_VERSION = agentData.op_mini.version_list[0].version; 12 | var CURRENT_VERSION = "CURRENT_VERSION"; 13 | 14 | helpers.setData = function(data) { 15 | browsers = data; 16 | }; 17 | 18 | // iOS apps use iOS user agent string but give no version info, so distribute based on known data 19 | helpers.distributeIOS = function() { 20 | if (!("Safari on iOS" in browsers) || !("iOS app" in browsers)) { 21 | return; 22 | } 23 | var iOSTotal = browsers["Safari on iOS"].total; 24 | var appTotal = browsers["iOS app"].total; 25 | 26 | // Distribute iOS app points 27 | for (var o in browsers["Safari on iOS"]) { 28 | var val = browsers["Safari on iOS"][o]; 29 | var ratio = val / iOSTotal; 30 | browsers["Safari on iOS"][o] += Math.round(ratio * appTotal); 31 | } 32 | delete browsers["iOS app"]; 33 | }; 34 | 35 | // Google Analytics has no info on Opera Mobile versions, so distribute equally 36 | helpers.distributeOperaMobile = function() { 37 | if (!("Opera Mobile" in browsers)) { 38 | return; 39 | } 40 | var operaMobileArr = []; 41 | var versions = agentData["op_mob"].version_list; 42 | var i; 43 | for (i = 0; i < versions.length; i++) { 44 | var operaMobile = versions[i]; 45 | if (operaMobile !== null) { 46 | operaMobileArr.push(operaMobile); 47 | } 48 | } 49 | var browsersStats = browsers["Opera Mobile"]; 50 | var part = Math.round(browsersStats.total / operaMobileArr.length); 51 | for (i = 0; i < operaMobileArr.length; i++) { 52 | var cur = operaMobileArr[i].version; 53 | browsersStats[cur] = part; 54 | } 55 | }; 56 | 57 | helpers.getIntVersion = function(versionString) { 58 | var m = versionString.match(/^(\d+?)\./); 59 | if (m && m.length > 1) { 60 | return +m[1] + ""; 61 | } 62 | }; 63 | 64 | /* Returns version as N.N */ 65 | helpers.getSubVersion = function(versionString) { 66 | var m = versionString.match(/^(\d+\.\d)/); 67 | if (m && m.length > 1) { 68 | return +m[1] + ""; 69 | } 70 | }; 71 | 72 | helpers.getOperaVersion = function(versionString) { 73 | var version = helpers.getSubVersion(versionString); 74 | if (version >= "9" && version < "9.5") { 75 | version = "9"; 76 | } 77 | version = helpers.getVersionMatch("opera", version); 78 | return version; 79 | }; 80 | 81 | helpers.getSafariVersion = function(versionString) { 82 | var parts = versionString.split("."); 83 | var major = parts[0]; 84 | var minor = parts[1]; 85 | var version = major + "." + minor; 86 | 87 | switch (version) { 88 | case "4.1": 89 | version = "4.0"; 90 | break; 91 | default: 92 | if (minor == "0") { 93 | version = major; 94 | } 95 | break; 96 | } 97 | return version; 98 | }; 99 | 100 | helpers.getAndroidVersion = function(versionString) { 101 | var version = helpers.getSubVersion(versionString); 102 | if (version > 3 && version < 4) { 103 | version = "3"; 104 | } 105 | version = helpers.getVersionMatch("android", version); 106 | return version; 107 | }; 108 | 109 | helpers.getIosSafariVersion = function(versionString) { 110 | var parts = versionString.split("."); 111 | var major = parts[0]; 112 | var minor = parts[1]; 113 | 114 | var version = major + "." + minor; 115 | version = helpers.getVersionMatch("ios_saf", version); 116 | return version; 117 | }; 118 | 119 | helpers.getChromeMapping = function(mapping, versionString) { 120 | var parts = versionString.split("."); 121 | var major = parseInt(parts[0]); 122 | var minor = parseInt(parts[1]); 123 | var entry = mapping.find(v => v[0] < major || (v[0] == major && v[1] <= minor)); 124 | 125 | if (entry) { 126 | return entry[2]; 127 | } 128 | }; 129 | 130 | helpers.getYaBrowserChromeMapping = function(versionString) { 131 | return helpers.getChromeMapping(yaBrowserMapping, versionString); 132 | }; 133 | 134 | helpers.getCocCocChromeMapping = function(versionString) { 135 | return helpers.getChromeMapping(cocCocMapping, versionString); 136 | }; 137 | 138 | helpers.getVersionMatch = function(browserId, versionString) { 139 | var version; 140 | var id = browserId + versionString; 141 | if (id in versionCache) { 142 | return versionCache[id]; 143 | } 144 | if (isNaN(versionString)) { 145 | versionCache[id] = versionString; 146 | return versionString; 147 | } 148 | 149 | var versionList = agentData[browserId].version_list; 150 | for (var i = 0; i < versionList.length; i++) { 151 | var ver = versionList[i].version; 152 | var range = ver.split("-"); 153 | if (range.length < 2) { 154 | // Single value, compare numerically 155 | if (+ver == +versionString) { 156 | version = ver; 157 | break; 158 | } else { 159 | continue; 160 | } 161 | } 162 | 163 | var start = +range[0]; 164 | var end = +range[1]; 165 | var versionNum = +versionString; 166 | if (versionNum >= start && versionNum <= end) { 167 | version = ver; 168 | break; 169 | } 170 | } 171 | var result = version || versionString; 172 | versionCache[id] = result; 173 | return result; 174 | }; 175 | 176 | function convertBrowserData(allData) { 177 | var ga_stats = allData.usage; 178 | var full_total = allData.total; 179 | var matched = 0; 180 | 181 | var newUsage = {}; 182 | var agentId; 183 | var amount; 184 | 185 | for (agentId in agentData) { 186 | var agdata = agentData[agentId]; 187 | var caniuseBrowser = agdata.browser; 188 | var usageByVersion = (newUsage[agentId] = {}); 189 | 190 | var versionList = agdata.version_list; 191 | var currentVersion = agdata.current_version; 192 | 193 | for (var i = 0; i < versionList.length; i++) { 194 | var versionNumber = versionList[i].version; 195 | usageByVersion[versionNumber] = 0; 196 | var browserGAStats = ga_stats[caniuseBrowser]; 197 | if (!browserGAStats) { 198 | continue; 199 | } 200 | if (currentVersion == versionNumber && browserGAStats[CURRENT_VERSION]) { 201 | browserGAStats[versionNumber] = browserGAStats[CURRENT_VERSION]; 202 | } 203 | if (browserGAStats[versionNumber]) { 204 | amount = browserGAStats[versionNumber]; 205 | var percentage = (amount / full_total) * 100; 206 | usageByVersion[versionNumber] = percentage; 207 | matched += amount; 208 | browserGAStats.total -= amount; 209 | delete browserGAStats[versionNumber]; 210 | if (currentVersion == versionNumber) { 211 | delete browserGAStats[CURRENT_VERSION]; 212 | } 213 | } 214 | } 215 | } 216 | 217 | allData.usage = newUsage; 218 | } 219 | 220 | function parse(entries) { 221 | var browsers = { others: 0 }; 222 | var other_total = 0; 223 | var tracked_total = 0; 224 | 225 | helpers.setData(browsers); 226 | 227 | for (var i = 0; i < entries.length; i++) { 228 | var entry = entries[i]; 229 | var os = entry[0]; 230 | var os_ver = entry[1]; 231 | var browser = entry[2]; 232 | var version = entry[3]; 233 | var isMobile = entry[4] === "mobile" || entry[4] === "tablet"; 234 | var pageviews = +entry[5]; 235 | 236 | if (browser == "Opera" && (isMobile || os == "(not set)")) { 237 | browser = "Opera Mobile"; 238 | } else if (os == "iOS" || os == "iPad" || os == "iPhone" || os == "iPod") { 239 | // all apps on ios must use safari engine by apple rules 240 | browser = "Safari on iOS"; 241 | } else if (browser == "Safari (in-app)") { 242 | browser = "Safari on iOS"; 243 | } else if (browser == "BlackBerry") { 244 | browser = "Blackberry Browser"; 245 | } else if (browser == "Internet Explorer") { 246 | browser = "IE"; 247 | } else if (browser == "UC Browser" && os == "Android") { 248 | browser = "UC Browser for Android"; 249 | } 250 | 251 | var v_num; 252 | var tracked = true; 253 | switch (browser) { 254 | case "Edge": 255 | browser = "Edge"; 256 | v_num = helpers.getIntVersion(version); 257 | break; 258 | 259 | case "Chrome": 260 | if (os == "Android") { 261 | browser += " for Android"; 262 | v_num = CURRENT_VERSION; 263 | } else { 264 | v_num = helpers.getIntVersion(version); 265 | } 266 | break; 267 | 268 | case "YaBrowser": 269 | // This is valid for both Desktop and Android (iOS is considered Safari) 270 | v_num = helpers.getYaBrowserChromeMapping(version); 271 | if (v_num) { 272 | browser = os == "Android" ? "Chrome for Android" : "Chrome"; 273 | } 274 | break; 275 | 276 | case "Coc Coc": 277 | // This is valid for both Desktop and Android (iOS is considered Safari) 278 | v_num = helpers.getCocCocChromeMapping(version); 279 | if (v_num) { 280 | browser = os == "Android" ? "Chrome for Android" : "Chrome"; 281 | } 282 | break; 283 | 284 | case "Opera": 285 | v_num = helpers.getOperaVersion(version); 286 | break; 287 | 288 | case "Firefox": 289 | if (os == "Android") { 290 | browser += " for Android"; 291 | v_num = CURRENT_VERSION; 292 | } else { 293 | v_num = helpers.getSubVersion(version); 294 | } 295 | break; 296 | 297 | case "IE": 298 | v_num = helpers.getSubVersion(version); 299 | break; 300 | 301 | case "Opera Mini": 302 | v_num = OPERA_MINI_VERSION; // getSubVersion(version); 303 | break; 304 | 305 | case "Opera Mobile": 306 | v_num = helpers.getSubVersion(version); 307 | if (!v_num) { 308 | v_num = "x"; 309 | } 310 | break; 311 | 312 | case "Safari": 313 | v_num = helpers.getSafariVersion(version); 314 | //if(v_num) b.total += pageviews; 315 | break; 316 | 317 | case "Android Browser": 318 | v_num = helpers.getAndroidVersion(os_ver); 319 | break; 320 | 321 | case "Android Webview": 322 | browser = "Android Browser"; 323 | // v_num = helpers.getIntVersion(version); // Use this once multi versions are available 324 | v_num = CURRENT_VERSION; 325 | break; 326 | 327 | case "Safari on iOS": 328 | v_num = helpers.getIosSafariVersion(os_ver); 329 | break; 330 | 331 | case "iOS app": 332 | v_num = "x"; 333 | break; 334 | 335 | case "Blackberry Browser": 336 | v_num = version.split(".")[0]; 337 | break; 338 | 339 | case "Samsung Internet": 340 | v_num = helpers.getVersionMatch("samsung", version); 341 | break; 342 | 343 | case "UC Browser for Android": 344 | v_num = CURRENT_VERSION; // helpers.getSubVersion(version); 345 | break; 346 | 347 | default: 348 | v_num = null; 349 | } 350 | 351 | if (!(browser in browsers)) { 352 | browsers[browser] = { total: 0 }; 353 | } 354 | var b = browsers[browser]; 355 | 356 | if (!v_num) { 357 | tracked = false; 358 | } 359 | 360 | if (v_num) { 361 | if (b[v_num]) { 362 | b[v_num] += pageviews; 363 | } else { 364 | b[v_num] = pageviews; 365 | } 366 | b.total += pageviews; 367 | } else { 368 | browsers.others += pageviews; 369 | if (!tracked) b.total += pageviews; 370 | } 371 | 372 | if (!tracked) { 373 | other_total += pageviews; 374 | } else { 375 | tracked_total += pageviews; 376 | } 377 | } 378 | 379 | // Remove insignificant browsers 380 | for (var o in browsers) { 381 | if (browsers[o].total < 10) { 382 | delete browsers[o]; 383 | } 384 | } 385 | 386 | // Distribute iOS points 387 | helpers.distributeIOS(); 388 | 389 | // Distribute Opera Mobile points 390 | helpers.distributeOperaMobile(); 391 | 392 | var full_total = other_total + tracked_total; 393 | 394 | var data_obj = { 395 | total: full_total, 396 | usage: browsers, 397 | }; 398 | 399 | convertBrowserData(data_obj); 400 | 401 | return data_obj.usage; 402 | } 403 | 404 | module.exports = { 405 | parse, 406 | helpers, 407 | }; 408 | -------------------------------------------------------------------------------- /src/google-analytics.js: -------------------------------------------------------------------------------- 1 | const { google } = require("googleapis"); 2 | 3 | const analytics = google.analytics("v3"); 4 | 5 | const getAccounts = auth => 6 | new Promise((resolve, reject) => { 7 | analytics.management.accounts.list({ auth }, (err, response) => { 8 | if (err) return reject(err); 9 | 10 | const results = response.data; 11 | const accounts = results.items; 12 | 13 | resolve(accounts); 14 | }); 15 | }); 16 | 17 | const getWebProperties = (auth, accountId) => 18 | new Promise((resolve, reject) => { 19 | analytics.management.webproperties.list({ auth, accountId }, (err, response) => { 20 | if (err) return reject(err); 21 | 22 | const results = response.data; 23 | const webProperties = results.items; 24 | 25 | resolve(webProperties); 26 | }); 27 | }); 28 | 29 | const getProfiles = (auth, accountId, webPropertyId) => 30 | new Promise((resolve, reject) => { 31 | analytics.management.profiles.list({ auth, accountId, webPropertyId }, (err, response) => { 32 | if (err) return reject(err); 33 | 34 | const results = response.data; 35 | const profiles = results.items; 36 | 37 | resolve(profiles); 38 | }); 39 | }); 40 | 41 | const getData = (auth, profileId, startDate, endDate) => 42 | new Promise((resolve, reject) => { 43 | const options = { 44 | auth, 45 | ids: `ga:${profileId}`, 46 | dimensions: [ 47 | "ga:operatingSystem", 48 | "ga:operatingSystemVersion", 49 | "ga:browser", 50 | "ga:browserVersion", 51 | "ga:deviceCategory", 52 | ].join(","), 53 | sort: "ga:browser", 54 | "max-results": 100000, 55 | metrics: "ga:pageviews", 56 | "start-date": startDate.toISOString().slice(0, 10), 57 | "end-date": endDate.toISOString().slice(0, 10), 58 | }; 59 | 60 | console.log("Getting data..."); 61 | 62 | analytics.data.ga.get(options, (err, response) => { 63 | if (err) return reject(err); 64 | 65 | const results = response.data; 66 | const data = results.rows || []; 67 | 68 | resolve(data); 69 | }); 70 | }); 71 | 72 | module.exports = { 73 | getAccounts, 74 | getWebProperties, 75 | getProfiles, 76 | getData, 77 | }; 78 | -------------------------------------------------------------------------------- /src/google-auth.js: -------------------------------------------------------------------------------- 1 | const portfinder = require("portfinder"); 2 | const listen = require("./listen"); 3 | const opener = require("opener"); 4 | const { google } = require("googleapis"); 5 | 6 | const clientId = 7 | process.env.BROWSERSLIST_GA_CLIENT_ID || 8 | "343796874716-6k918h5uajk7k3apdua9n8m6her4igv7.apps.googleusercontent.com"; 9 | const accessToken = process.env.BROWSERSLIST_GA_ACCESS_TOKEN; 10 | const refreshToken = process.env.BROWSERSLIST_GA_REFRESH_TOKEN; 11 | 12 | const googleAuth = callback => { 13 | portfinder.getPort((err, port) => { 14 | if (err) { 15 | return console.error(err); 16 | } 17 | 18 | const redirectUrl = `http://127.0.0.1:${port}`; 19 | const oauth2Client = new google.auth.OAuth2(clientId, null, redirectUrl); 20 | 21 | const handleAuth = (tokens, callback) => { 22 | oauth2Client.setCredentials(tokens); 23 | callback(oauth2Client); 24 | }; 25 | 26 | if (accessToken && refreshToken) { 27 | return handleAuth({ access_token: accessToken, refresh_token: refreshToken }, callback); 28 | } 29 | 30 | const url = oauth2Client.generateAuthUrl({ 31 | scope: "https://www.googleapis.com/auth/analytics.readonly", 32 | }); 33 | 34 | console.log("Please open this URL in your browser:", url); 35 | try { 36 | opener(url); 37 | } catch (e) { 38 | /* User will have to open manually */ 39 | } 40 | 41 | listen(redirectUrl, port, code => { 42 | oauth2Client.getToken(code, (err, tokens) => { 43 | if (err) { 44 | return console.error(err); 45 | } 46 | 47 | handleAuth(tokens, callback); 48 | }); 49 | }); 50 | }); 51 | }; 52 | 53 | module.exports = googleAuth; 54 | -------------------------------------------------------------------------------- /src/listen.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const { URL } = require("url"); 3 | 4 | const template = message => ` 5 | 6 | 7 | 8 | 9 |${message}
14 | 15 | 16 | `; 17 | 18 | const listen = (redirectUrl, port, callback) => { 19 | let server; 20 | 21 | const requestHandler = (request, response) => { 22 | const code = new URL(redirectUrl + request.url).searchParams.get("code"); 23 | 24 | if (code) { 25 | response.end(template("✅ Success! You can close this tab and go back to the terminal.")); 26 | callback(code); 27 | server.close(); 28 | } else { 29 | response.end(template("❌ Unable to retrieve authorization code.")); 30 | } 31 | }; 32 | 33 | server = http.createServer(requestHandler); 34 | 35 | server.listen(port, err => { 36 | if (err) { 37 | return console.error("An error has occurred:", err); 38 | } 39 | }); 40 | }; 41 | 42 | module.exports = listen; 43 | -------------------------------------------------------------------------------- /tests/caniuse-agent-data.test.js: -------------------------------------------------------------------------------- 1 | var agentData = require("../src/caniuse-agent-data"); 2 | 3 | test("browsers tracked are the same 19 as caniuse", () => { 4 | const browsers = Object.keys(agentData); 5 | expect(browsers.length).toBe(19); 6 | }); 7 | -------------------------------------------------------------------------------- /tests/caniuse-parser.test.js: -------------------------------------------------------------------------------- 1 | var { helpers } = require("../src/caniuse-parser"); 2 | 3 | test("invalid YaBrowser version is mapped to undefined", () => { 4 | const chromeVersion = helpers.getYaBrowserChromeMapping("1"); 5 | expect(chromeVersion).toBeUndefined(); 6 | }); 7 | 8 | test("exact YaBrowser version is mapped to the right Chrome version", () => { 9 | const chromeVersion = helpers.getYaBrowserChromeMapping("18.1"); 10 | expect(chromeVersion).toBe(63); 11 | }); 12 | 13 | test("non-existing YaBrowser version is mapped to the older nearest Chrome version", () => { 14 | const chromeVersion = helpers.getYaBrowserChromeMapping("18"); 15 | expect(chromeVersion).toBe(62); 16 | }); 17 | --------------------------------------------------------------------------------