Code Monkey home page Code Monkey logo

browserslist-ga's Introduction

Browserslist-GA logo

Target browsers tailored to your audience.


Interested in a bot that does all of this for you? Click here!


How to use

In the root directory of your project run:

npx browserslist-ga

(npx comes with npm 5.2+, for older versions run npm install -g browserslist-ga and then browserslist-ga)
(to run the latest code directly from GitHub, execute npx github:browserslist/browserslist-ga instead)

You'll be asked to login with your Google Account (please see this issue 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:

> 0.5% in my stats  # Or a different percentage

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.

Why should I care?

Browsers update very often these days, with major releases getting published every month. With each new browser version comes support for new web platform features. Thanks to open source projects such as Autoprefixer and Babel we are able to use these features while supporting older browsers. But this backward compatibility comes with a cost. We can't really keep adding prefixes, polyfills and other fallbacks to support every browser ever invented.

Browserslist is an open source project that can minimize those costs by allowing you to configure which browsers you care about. It is supported by tools such as Autoprefixer, babel-preset-env, postcss-normalize and many others. Here's how you configure Browserslist:

> 1%              # I want to support browser versions that have more than 1% of global usage
Last 2 versions   # And the latest 2 versions of each browser
IE 9              # And also Internet Explorer 9 specifically

The global browser usage data comes from caniuse.com and is downloaded from npm when you run npm install. Package managers such as npm and Yarn will generate a lockfile with the exact version of each package that was installed. This means the caniuse database that is used to perform these queries will always be the same. 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. Apart from remembering to update this package, there's something else you should consider:

  • For instance, in China there are some popular browsers that are not used in the US and Europe.
  • Or maybe your audience uses mostly mobile browsers.
  • Or maybe you are building an application for the government and need to support Internet Explorer 8.

The point being, it's important to make decisions based on your audience. Browserslist-GA aims to help you with that. It integrates Google Analytics with Browserslist to keep your targeted browsers updated.

Notes

There are some differences compared to the caniuse Google Analytics importer:

  • 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).
  • 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).
  • Just like for YaBrowser, the same approach is applied to the Coc Coc browser.

Kudos

All the praise goes to the humans and martians that develop and maintain Can I Use and Browserslist.

browserslist-ga's People

Contributors

dependabot[bot] avatar dmfrancisco avatar elstgav avatar georgetaveras1231 avatar ogonkov avatar ssong avatar vasfed avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

browserslist-ga's Issues

Universal Analytics is deprecated

According to official documentation, Universal Analytics is going to be deprecated and replaced by GA4 by 1st July 2023. This means that the current solution for browserslist-ga is going to be deprecated too since it depends on GA3.

I have started working on a PR which uses GA4, however I have noticed that the new Google Analytics Data API does not include the browser version dimension in the API (see Stack Overflow).

Is there a known solution or workaround? Is this project going to be deprecated?

Thank you. 🙏

Do not require Google login

Supposing that a user does not want to log in to a Google account to take advantage of this tool, is there a possibility that this tool could allow the user to specify the path to a manually exported Google Analytics custom report instead, e.g., browserslist-ga --path my-manual-ga-export.csv? Google Analytics allows exporting of custom reports, so I imagine this tool's documentation could specify the format of said custom report such that this tool could expect and work with a predictable format when the path is specified.

Object.entries is not a function

Not sure if you want to add a note to the readme to say that this requires npm >= 7 or if you want to update to remove/polyfill Object.entries?

Getting error 404 "is not in the npm registry"

I cloned the repo and ran npx browserlist-ga as it says in the readme.

This is the response:

# neejoh at mac.local in ~/Applications/Analytics/browserslist-ga on git:master ● [8:58:24]
→ npx browserlist-ga
npm ERR! code E404
npm ERR! 404 Not Found - GET https://registry.npmjs.org/browserlist-ga - Not found
npm ERR! 404
npm ERR! 404  'browserlist-ga@latest' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/neejoh/.npm/_logs/2020-02-25T07_58_32_952Z-debug.log
Install for [ 'browserlist-ga@latest' ] failed with code 1

Am I missing something?

Sign in with Google not working

$npx browserslist-ga npx: installed 83 in 8.976s Please open this URL in your browser: https://accounts.google.com/o/oauth2/v2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fanalytics.readonly&response_type=code&client_id=343796874716-6k918h5uajk7k3apdua9n8m6her4igv7.apps.googleusercontent.com&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000

Open URL, log into Google:
image
Sign in with Google temporarily disabled for this app This app has not been verified yet by Google in order to use Google Sign In.

Publish 0.0.13?

Hi,

I'm currently playing around with browserslist-ga-export which relies on browserslist-ga under the hood. Because the iOS Safari Usage fix was merged in but not released as a new patch version, usage of browserslist-ga-export is not correctly parsing iOS Safari.

Can we release a new patch version of browserslist-ga? I've modified the content in node_modules to match that PR as a temporary workaround but I'm also writing up steps to reproduce the work for generating the data and this is currently a blocker. Thanks!

Last generated date?

It would be useful to know the date the browserlist was generated from GA data.
Could the generated browserslist-stats.json file have a timestamp field at the top indicating when it was generated? Or would there be a better way to know what date a particular browserlist's data was made from?

error_description: 'client_secret is missing.'

I created my own GoogleApp with Client id OAUTH (Web Client and OAuth Client) with this scope/permision:

  • email
  • profle
  • openid
  • ../auth/analytics.readonly
  • ../auth/analytics
  • ../auth/analytics.edit
  • ../auth/analytics.manage.users
  • ../auth/analytics.manage.users.readonly
  • ../auth/analytics.provision
  • ../auth/analytics.user.deletion
    And this http://127.0.0.1:8000 origin Javascript and URI

But, after login and view the "Success! You can close this tab and go back to the terminal." in my terminal show this erro:

{ Error: invalid_request
    at createError (/home/luisangonzalez/.npm/_npx/15390/lib/node_modules/browserslist-ga-qdq/node_modules/axios/lib/core/createError.js:16:15)
    at settle (/home/luisangonzalez/.npm/_npx/15390/lib/node_modules/browserslist-ga-qdq/node_modules/axios/lib/core/settle.js:18:12)
    at IncomingMessage.handleStreamEnd (/home/luisangonzalez/.npm/_npx/15390/lib/node_modules/browserslist-ga-qdq/node_modules/axios/lib/adapters/http.js:201:11)
    at emitNone (events.js:111:20)
    at IncomingMessage.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:139:11)
    at process._tickCallback (internal/process/next_tick.js:181:9)
  config: 
   { adapter: [Function: httpAdapter],
     transformRequest: { '0': [Function: transformRequest] },
     transformResponse: { '0': [Function: transformResponse] },
     timeout: 0,
     xsrfCookieName: 'XSRF-TOKEN',
     xsrfHeaderName: 'X-XSRF-TOKEN',
     maxContentLength: -1,
     validateStatus: [Function: validateStatus],
     headers: 
      { Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': 'google-api-nodejs-client/1.6.1',
        'Content-Length': 282 },
     method: 'post',
     url: 'https://www.googleapis.com/oauth2/v4/token',
     data: 'code=4%2FlQD0TYBTL8wWAJS6gYgh_w19ytTOWK8b_u1hzxMkwNDyKoLDDkLE9KxYJNFNLLUuXt07pUspLJ6Lr7T2Rm3DDoo&client_id=827156059949-ev07hth0oqc5bdsjrl822mbqtibobm52.apps.googleusercontent.com&client_secret=&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000&grant_type=authorization_code&code_verifier=' },
  request: 
   ClientRequest {
     domain: null,
     _events: 
      { socket: [Function],
        abort: [Function],
        aborted: [Function],
        error: [Function],
        timeout: [Function],
        prefinish: [Function: requestOnPrefinish] },
     _eventsCount: 6,
     _maxListeners: undefined,
     output: [],
     outputEncodings: [],
     outputCallbacks: [],
     outputSize: 0,
     writable: true,
     _last: true,
     upgrading: false,
     chunkedEncoding: false,
     shouldKeepAlive: false,
     useChunkedEncodingByDefault: true,
     sendDate: false,
     _removedConnection: false,
     _removedContLen: false,
     _removedTE: false,
     _contentLength: null,
     _hasBody: true,
     _trailer: '',
     finished: true,
     _headerSent: true,
     socket: 
      TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: 'www.googleapis.com',
        npnProtocol: false,
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object],
        _eventsCount: 9,
        connecting: false,
        _hadError: false,
        _handle: null,
        _parent: null,
        _host: 'www.googleapis.com',
        _readableState: [Object],
        readable: false,
        domain: null,
        _maxListeners: undefined,
        _writableState: [Object],
        writable: false,
        allowHalfOpen: false,
        _bytesDispatched: 518,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: undefined,
        _server: null,
        ssl: null,
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        write: [Function: writeAfterFIN],
        _idleNext: null,
        _idlePrev: null,
        _idleTimeout: -1,
        [Symbol(asyncId)]: 166,
        [Symbol(bytesRead)]: 480 },
     connection: 
      TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: 'www.googleapis.com',
        npnProtocol: false,
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object],
        _eventsCount: 9,
        connecting: false,
        _hadError: false,
        _handle: null,
        _parent: null,
        _host: 'www.googleapis.com',
        _readableState: [Object],
        readable: false,
        domain: null,
        _maxListeners: undefined,
        _writableState: [Object],
        writable: false,
        allowHalfOpen: false,
        _bytesDispatched: 518,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: undefined,
        _server: null,
        ssl: null,
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        write: [Function: writeAfterFIN],
        _idleNext: null,
        _idlePrev: null,
        _idleTimeout: -1,
        [Symbol(asyncId)]: 166,
        [Symbol(bytesRead)]: 480 },
     _header: 'POST /oauth2/v4/token HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: google-api-nodejs-client/1.6.1\r\nContent-Length: 282\r\nHost: www.googleapis.com\r\nConnection: close\r\n\r\n',
     _onPendingData: [Function: noopPendingOutput],
     agent: 
      Agent {
        domain: null,
        _events: [Object],
        _eventsCount: 1,
        _maxListeners: undefined,
        defaultPort: 443,
        protocol: 'https:',
        options: [Object],
        requests: {},
        sockets: [Object],
        freeSockets: {},
        keepAliveMsecs: 1000,
        keepAlive: false,
        maxSockets: Infinity,
        maxFreeSockets: 256,
        maxCachedSessions: 100,
        _sessionCache: [Object] },
     socketPath: undefined,
     timeout: undefined,
     method: 'POST',
     path: '/oauth2/v4/token',
     _ended: true,
     res: 
      IncomingMessage {
        _readableState: [Object],
        readable: false,
        domain: null,
        _events: [Object],
        _eventsCount: 3,
        _maxListeners: undefined,
        socket: [Object],
        connection: [Object],
        httpVersionMajor: 1,
        httpVersionMinor: 1,
        httpVersion: '1.1',
        complete: true,
        headers: [Object],
        rawHeaders: [Array],
        trailers: {},
        rawTrailers: [],
        upgrade: false,
        url: '',
        method: null,
        statusCode: 400,
        statusMessage: 'Bad Request',
        client: [Object],
        _consuming: true,
        _dumped: false,
        req: [Circular],
        responseUrl: 'https://www.googleapis.com/oauth2/v4/token',
        redirects: [],
        read: [Function] },
     aborted: undefined,
     timeoutCb: null,
     upgradeOrConnect: false,
     parser: null,
     maxHeadersCount: null,
     _redirectable: 
      Writable {
        _writableState: [Object],
        writable: true,
        domain: null,
        _events: [Object],
        _eventsCount: 2,
        _maxListeners: undefined,
        _options: [Object],
        _redirectCount: 0,
        _redirects: [],
        _requestBodyLength: 282,
        _requestBodyBuffers: [],
        _onNativeResponse: [Function],
        _currentRequest: [Circular],
        _currentUrl: 'https://www.googleapis.com/oauth2/v4/token' },
     [Symbol(outHeadersKey)]: 
      { accept: [Array],
        'content-type': [Array],
        'user-agent': [Array],
        'content-length': [Array],
        host: [Array] } },
  response: 
   { status: 400,
     statusText: 'Bad Request',
     headers: 
      { 'content-type': 'application/json; charset=utf-8',
        vary: 'X-Origin, Referer, Origin,Accept-Encoding',
        date: 'Wed, 14 Nov 2018 09:06:27 GMT',
        server: 'ESF',
        'cache-control': 'private',
        'x-xss-protection': '1; mode=block',
        'x-frame-options': 'SAMEORIGIN',
        'x-content-type-options': 'nosniff',
        'alt-svc': 'quic=":443"; ma=2592000; v="44,43,39,35"',
        'accept-ranges': 'none',
        connection: 'close' },
     config: 
      { adapter: [Function: httpAdapter],
        transformRequest: [Object],
        transformResponse: [Object],
        timeout: 0,
        xsrfCookieName: 'XSRF-TOKEN',
        xsrfHeaderName: 'X-XSRF-TOKEN',
        maxContentLength: -1,
        validateStatus: [Function: validateStatus],
        headers: [Object],
        method: 'post',
        url: 'https://www.googleapis.com/oauth2/v4/token',
        data: 'code=4%2FlQD0TYBTL8wWAJS6gYgh_w19ytTOWK8b_u1hzxMkwNDyKoLDDkLE9KxYJNFNLLUuXt07pUspLJ6Lr7T2Rm3DDoo&client_id=827156059949-ev07hth0oqc5bdsjrl822mbqtibobm52.apps.googleusercontent.com&client_secret=&redirect_uri=http%3A%2F%2F127.0.0.1%3A8000&grant_type=authorization_code&code_verifier=' },
     request: 
      ClientRequest {
        domain: null,
        _events: [Object],
        _eventsCount: 6,
        _maxListeners: undefined,
        output: [],
        outputEncodings: [],
        outputCallbacks: [],
        outputSize: 0,
        writable: true,
        _last: true,
        upgrading: false,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [Object],
        connection: [Object],
        _header: 'POST /oauth2/v4/token HTTP/1.1\r\nAccept: application/json, text/plain, */*\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: google-api-nodejs-client/1.6.1\r\nContent-Length: 282\r\nHost: www.googleapis.com\r\nConnection: close\r\n\r\n',
        _onPendingData: [Function: noopPendingOutput],
        agent: [Object],
        socketPath: undefined,
        timeout: undefined,
        method: 'POST',
        path: '/oauth2/v4/token',
        _ended: true,
        res: [Object],
        aborted: undefined,
        timeoutCb: null,
        upgradeOrConnect: false,
        parser: null,
        maxHeadersCount: null,
        _redirectable: [Object],
        [Symbol(outHeadersKey)]: [Object] },
     data: 
      { error: 'invalid_request',
        error_description: 'client_secret is missing.' } },
  code: '400' }

And not generate .json

--

empty ios_saf counts

ios_saf is current showing 0's for all versions.
A quick glance looks like caniuse_parser.js is keying on 'iOS Safari' whereas const { agents } = require("caniuse-lite/dist/unpacker/agents"); agents.ios_saf.browser; is 'Safari on iOS'

Stats based on parameters other than usage

Hello, first off, this is a great tool and it's super easy to use - thank you.

I would love it if the tool could get stats based on something other than sessions? For example, I know that we have to support IE9 because we have a significant amount of revenue attached to a customer who cannot upgrade. This revenue is shown in GA as a goal value, so could the percentages be calculated based on that instead?

Update package-lock.json file to automatically remove the high severity vulnerability introduced by package node-forge

Hi, @dmfrancisco, I have reported a vulnerability issue in package google-p12-pem.

As far as I am aware, vulnerability(high severity) CVE-2020-7720 detected in package node-forge(<0.10.0) is directly referenced by  [email protected], on which your package [email protected] transitively depends. As such, this vulnerability can also affect [email protected] via the following path:
[email protected][email protected][email protected][email protected][email protected][email protected](vulnerable version)

Since google-p12-pem has released a new patched version [email protected] to resolve this issue ([email protected][email protected](fix version)), then this vulnerability patch can be automatically propagated into your project only if you update your package-lock.json file (delete package-lock.json and re-execute npm install command):
[email protected][email protected][email protected][email protected][email protected][email protected](vulnerability fix version).

A warm tip.^_^
Best regards,
Paimon

browserslist-ga output does not sum to 100%

I'm not sure if this is a bug but it seems wrong that the output of browserslist-ga does not sum to 100%.

This is then confusing when you run a browserslist query with the using the file with the --stats option

Please find for example the output I had for my website, which sums to ~48.5%

browserslist-stats.zip

To get those results, I enabled the analytics api for my project and created an oauth client id, then configured my terminal with it and ran npx browserslist-ga

Google blocks the app

For some reason the app is blocked by Google, I am getting this error when trying to connect the app to my Google account:

Logging with Google account is temporary blocked for this application
You can't log into the app using Google account because the app has not yet been verified

Or something like that, I have the warning in Russian, so it's not worded exactly like that.

Everywhere 0 if views > 1000

This stat parsing is not correct:
Windows,7,Chrome,79.0.3945.130,desktop,401 754

Speed fix: re-save csv file without space delimiters
Windows,7,Chrome,79.0.3945.130,desktop,401754

Account for deprecated isMobile dimension

Following up on browserslist/browserslist-ga-export#3, the isMobile dimension being used by this package is deprecated per https://developers.google.com/analytics/devguides/reporting/core/v2/changelog.

I believe changes would only need to be made in the following places:

https://developers.google.com/analytics/devguides/reporting/core/v2/changelog suggests that it would be as simple as replacing ga:isMobile with ga:deviceCategory and entry[4] == "Yes" with entry[4] == "mobile", but https://analyticscanvas.com/google-analytics-ismobile-istablet-left-building/ suggests that it should be entry[4] == "mobile" || entry[4] == "tablet". Perhaps further research is needed on this point...

Also, the deprecation notice was published back in 2012 and according to the data deprecation policy, the dimension should have been removed a while ago, so not sure how urgent this is... but there does seem to be at least some path to migrating to the new dimension.

Samsung Internet data missing from JSON output

Hello,

I was looking into using this tool but the resulting browserslist-stats.json data is outputting 0 for all Samsung Internet (SI) versions:

"samsung": {
    "4": 0,
    "5": 0,
    "6.2": 0,
    "7.2": 0
  },

I'm using this tool to generate the JSON from a CSV rather than login directly. It pulls in the caniuse-parser.js. I can see the correct SI data in the CSV, just 0 in the output data. I believe this is because Samsung Internet isn't being parsed in this file.

Thanks!

Stats add up to 92%

Hello, this tool is great! I'm looking for a way to generate a coverage number based on real usage. But when I import data from GA, the max coverage I can seem to get is around 92%

$ npx browserslist --coverage --stats="./browserslist-stats.json" "cover 100% in my stats"
These browsers account for 92.41% of all users in custom statistics

Which makes sense because the numbers in the stats file don't add up to 100:

> var stats = require('./browserslist-stats.json')
> Object.values(stats).reduce((acc, ss) => acc.concat(Object.values(ss)), []).reduce((acc, n) => acc + n, 0)
92.40687679083089

Is this expected? Where are the 8% are disappearing to?

Error 400 - The loopback flow has been blocked

When attempting to use the app, with the default BROWSERSLIST_GA_CLIENT_ID a 400 - Invalid Request error.

The loopback flow has been blocked in order to keep users secure. Follow the Loopback IP Address flow migration guide linked in the developer docs below to migrate your app to an alternative method.
Related developer documentation

From my understand, Google OAuth2 no longer supports redirecting back to http://127.0.0.1 or http://localhost unless the app is OAuth Client App is specified as Desktop.

image

I have created my own app and am able to work without issue but it might make sense to update the default OAuth Client ID.

Headless ?

Is there a way to use, say, env variables to update the file in pure headless mode ?

This would allow hooking it in our CI pipeline, thus updating stats every push in the process

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.